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
data/lib/expressir/coverage.rb
CHANGED
|
@@ -3,53 +3,39 @@ require "pathname"
|
|
|
3
3
|
module Expressir
|
|
4
4
|
# Coverage module for calculating documentation coverage of EXPRESS entities
|
|
5
5
|
module Coverage
|
|
6
|
-
#
|
|
7
|
-
ENTITY_TYPE_MAP
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
"Parameter" => "PARAMETER",
|
|
40
|
-
"Variable" => "VARIABLE",
|
|
41
|
-
"Attribute" => "ATTRIBUTE",
|
|
42
|
-
"DerivedAttribute" => "DERIVED_ATTRIBUTE",
|
|
43
|
-
"InverseAttribute" => "INVERSE_ATTRIBUTE",
|
|
44
|
-
"UniqueRule" => "UNIQUE_RULE",
|
|
45
|
-
"WhereRule" => "WHERE_RULE",
|
|
46
|
-
"EnumerationItem" => "ENUMERATION_ITEM",
|
|
47
|
-
"Interface" => "INTERFACE",
|
|
48
|
-
"InterfaceItem" => "INTERFACE_ITEM",
|
|
49
|
-
"InterfacedItem" => "INTERFACED_ITEM",
|
|
50
|
-
"SchemaVersion" => "SCHEMA_VERSION",
|
|
51
|
-
"SchemaVersionItem" => "SCHEMA_VERSION_ITEM",
|
|
52
|
-
}.freeze
|
|
6
|
+
# Single source of truth: EXPRESS type name → Ruby class name pairs.
|
|
7
|
+
# ENTITY_TYPE_MAP and CLASS_TO_EXPRESS_TYPE_MAP are derived from this
|
|
8
|
+
# so the two never drift out of sync.
|
|
9
|
+
TYPE_PAIRS = [
|
|
10
|
+
["TYPE", "Expressir::Model::Declarations::Type"],
|
|
11
|
+
["ENTITY", "Expressir::Model::Declarations::Entity"],
|
|
12
|
+
["CONSTANT", "Expressir::Model::Declarations::Constant"],
|
|
13
|
+
["FUNCTION", "Expressir::Model::Declarations::Function"],
|
|
14
|
+
["RULE", "Expressir::Model::Declarations::Rule"],
|
|
15
|
+
["PROCEDURE", "Expressir::Model::Declarations::Procedure"],
|
|
16
|
+
["SUBTYPE_CONSTRAINT", "Expressir::Model::Declarations::SubtypeConstraint"],
|
|
17
|
+
["PARAMETER", "Expressir::Model::Declarations::Parameter"],
|
|
18
|
+
["VARIABLE", "Expressir::Model::Declarations::Variable"],
|
|
19
|
+
["ATTRIBUTE", "Expressir::Model::Declarations::Attribute"],
|
|
20
|
+
["DERIVED_ATTRIBUTE", "Expressir::Model::Declarations::DerivedAttribute"],
|
|
21
|
+
["INVERSE_ATTRIBUTE", "Expressir::Model::Declarations::InverseAttribute"],
|
|
22
|
+
["UNIQUE_RULE", "Expressir::Model::Declarations::UniqueRule"],
|
|
23
|
+
["WHERE_RULE", "Expressir::Model::Declarations::WhereRule"],
|
|
24
|
+
["ENUMERATION_ITEM", "Expressir::Model::DataTypes::EnumerationItem"],
|
|
25
|
+
["INTERFACE", "Expressir::Model::Declarations::Interface"],
|
|
26
|
+
["INTERFACE_ITEM", "Expressir::Model::Declarations::InterfaceItem"],
|
|
27
|
+
["INTERFACED_ITEM", "Expressir::Model::Declarations::InterfacedItem"],
|
|
28
|
+
["SCHEMA_VERSION", "Expressir::Model::Declarations::SchemaVersion"],
|
|
29
|
+
["SCHEMA_VERSION_ITEM", "Expressir::Model::Declarations::SchemaVersionItem"],
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
# EXPRESS entity type name → Ruby class name.
|
|
33
|
+
ENTITY_TYPE_MAP = TYPE_PAIRS.to_h.freeze
|
|
34
|
+
|
|
35
|
+
# Short Ruby class name → EXPRESS entity type name.
|
|
36
|
+
CLASS_TO_EXPRESS_TYPE_MAP = TYPE_PAIRS.to_h do |express_name, ruby_class|
|
|
37
|
+
[ruby_class.split("::").last, express_name]
|
|
38
|
+
end.freeze
|
|
53
39
|
|
|
54
40
|
# Available TYPE subtypes based on data types
|
|
55
41
|
TYPE_SUBTYPES = %w[
|
|
@@ -401,78 +387,24 @@ module Expressir
|
|
|
401
387
|
entities.concat(container.unique_rules) if container.unique_rules
|
|
402
388
|
entities.concat(container.where_rules) if container.where_rules
|
|
403
389
|
|
|
404
|
-
when Expressir::Model::Declarations::Function
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
container.types&.each do |type|
|
|
417
|
-
entities.concat(find_nested_entities(type))
|
|
418
|
-
end
|
|
419
|
-
container.entities&.each do |entity|
|
|
420
|
-
entities.concat(find_nested_entities(entity))
|
|
421
|
-
end
|
|
422
|
-
container.functions&.each do |function|
|
|
423
|
-
entities.concat(find_nested_entities(function))
|
|
424
|
-
end
|
|
425
|
-
container.procedures&.each do |procedure|
|
|
426
|
-
entities.concat(find_nested_entities(procedure))
|
|
427
|
-
end
|
|
428
|
-
|
|
429
|
-
when Expressir::Model::Declarations::Rule
|
|
430
|
-
# Rule nested entities
|
|
431
|
-
entities.concat(container.variables) if container.variables
|
|
432
|
-
entities.concat(container.constants) if container.constants
|
|
433
|
-
entities.concat(container.types) if container.types
|
|
434
|
-
entities.concat(container.entities) if container.entities
|
|
435
|
-
entities.concat(container.functions) if container.functions
|
|
436
|
-
entities.concat(container.procedures) if container.procedures
|
|
437
|
-
entities.concat(container.subtype_constraints) if container.subtype_constraints
|
|
438
|
-
|
|
439
|
-
# Recursively find nested entities in nested containers
|
|
440
|
-
container.types&.each do |type|
|
|
441
|
-
entities.concat(find_nested_entities(type))
|
|
442
|
-
end
|
|
443
|
-
container.entities&.each do |entity|
|
|
444
|
-
entities.concat(find_nested_entities(entity))
|
|
445
|
-
end
|
|
446
|
-
container.functions&.each do |function|
|
|
447
|
-
entities.concat(find_nested_entities(function))
|
|
448
|
-
end
|
|
449
|
-
container.procedures&.each do |procedure|
|
|
450
|
-
entities.concat(find_nested_entities(procedure))
|
|
390
|
+
when Expressir::Model::Declarations::Function,
|
|
391
|
+
Expressir::Model::Declarations::Procedure,
|
|
392
|
+
Expressir::Model::Declarations::Rule
|
|
393
|
+
# All three scope containers follow the same nested-entity pattern.
|
|
394
|
+
# Derive the collection list from the model's own declaration rather
|
|
395
|
+
# than maintaining three separate hand-coded branches.
|
|
396
|
+
nested_attrs = container.class.collection_attributes_list - %i[
|
|
397
|
+
statements remark_items where_rules informal_propositions applies_to
|
|
398
|
+
]
|
|
399
|
+
nested_attrs.each do |attr|
|
|
400
|
+
collection = container.public_send(attr)
|
|
401
|
+
entities.concat(collection) if collection.is_a?(Array)
|
|
451
402
|
end
|
|
452
403
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
entities.concat(container.constants) if container.constants
|
|
458
|
-
entities.concat(container.types) if container.types
|
|
459
|
-
entities.concat(container.entities) if container.entities
|
|
460
|
-
entities.concat(container.functions) if container.functions
|
|
461
|
-
entities.concat(container.procedures) if container.procedures
|
|
462
|
-
entities.concat(container.subtype_constraints) if container.subtype_constraints
|
|
463
|
-
|
|
464
|
-
# Recursively find nested entities in nested containers
|
|
465
|
-
container.types&.each do |type|
|
|
466
|
-
entities.concat(find_nested_entities(type))
|
|
467
|
-
end
|
|
468
|
-
container.entities&.each do |entity|
|
|
469
|
-
entities.concat(find_nested_entities(entity))
|
|
470
|
-
end
|
|
471
|
-
container.functions&.each do |function|
|
|
472
|
-
entities.concat(find_nested_entities(function))
|
|
473
|
-
end
|
|
474
|
-
container.procedures&.each do |procedure|
|
|
475
|
-
entities.concat(find_nested_entities(procedure))
|
|
404
|
+
# Recursively find nested entities in nested scope containers
|
|
405
|
+
%i[types entities functions procedures].each do |attr|
|
|
406
|
+
collection = container.public_send(attr)
|
|
407
|
+
collection&.each { |item| entities.concat(find_nested_entities(item)) }
|
|
476
408
|
end
|
|
477
409
|
|
|
478
410
|
when Expressir::Model::Declarations::Interface
|
|
@@ -7,10 +7,34 @@ module Expressir
|
|
|
7
7
|
# This is the ONLY way to build models from AST - no Transformer fallback.
|
|
8
8
|
module Builder
|
|
9
9
|
class << self
|
|
10
|
-
|
|
10
|
+
# Thread-local key under which the current BuilderContext is stored
|
|
11
|
+
# for the duration of a build_with_remarks call. Recursive build
|
|
12
|
+
# calls read source/include_source from here instead of from
|
|
13
|
+
# class-level mutable state.
|
|
14
|
+
CONTEXT_KEY = :expressir_builder_context
|
|
15
|
+
|
|
16
|
+
# Returns the BuilderContext for the current thread, or nil if no
|
|
17
|
+
# build is in progress.
|
|
18
|
+
def current_context
|
|
19
|
+
Thread.current[CONTEXT_KEY]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Convenience accessors — read from the current context.
|
|
23
|
+
def source
|
|
24
|
+
current_context&.source
|
|
25
|
+
end
|
|
11
26
|
|
|
12
|
-
|
|
13
|
-
|
|
27
|
+
def include_source
|
|
28
|
+
current_context&.include_source
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Thread-local snake_case conversion cache. Thread-local avoids the
|
|
32
|
+
# mutable-constant anti-pattern while remaining thread-safe.
|
|
33
|
+
# Each thread gets its own cache; the cache grows with the number of
|
|
34
|
+
# unique AST node-type names encountered (bounded by grammar size).
|
|
35
|
+
def snake_case_cache
|
|
36
|
+
Thread.current[:expressir_snake_case_cache] ||= {}
|
|
37
|
+
end
|
|
14
38
|
|
|
15
39
|
# Register a builder for a node type.
|
|
16
40
|
# @param node_type [Symbol] The AST node type
|
|
@@ -23,32 +47,10 @@ module Expressir
|
|
|
23
47
|
|
|
24
48
|
# Build a Model object from AST data.
|
|
25
49
|
# @param ast [Hash] The AST with node type as key
|
|
26
|
-
# @param source [String, nil] The original source code
|
|
27
|
-
# @param include_source [Boolean, nil] Whether to include source
|
|
28
50
|
# @return [Model::ModelElement] The built model object
|
|
29
|
-
|
|
30
|
-
# When these appear as the first key in a multi-key hash, they should be
|
|
31
|
-
# skipped in favor of the content key. This handles grammar patterns like
|
|
32
|
-
# `element >> (op_comma >> element).repeat` which produce
|
|
33
|
-
# {:op_comma => ..., :element => {...}}.
|
|
34
|
-
OPERATOR_TOKENS = Set.new(%i[
|
|
35
|
-
op_comma op_colon op_decl op_delim op_leftparen op_rightparen
|
|
36
|
-
op_leftbracket op_rightbracket op_left_curly_brace op_right_curly_brace
|
|
37
|
-
op_period op_pipe op_double_backslash op_double_pipe op_double_asterisk
|
|
38
|
-
op_asterisk op_slash op_plus op_minus op_less_equal op_greater_equal
|
|
39
|
-
op_less_greater op_less_than op_greater_than op_equals
|
|
40
|
-
op_colon_less_greater_colon op_colon_equals_colon
|
|
41
|
-
op_query_begin op_query_end op_question_mark
|
|
42
|
-
]).freeze
|
|
43
|
-
|
|
44
|
-
def build(ast, source: nil, include_source: nil)
|
|
51
|
+
def build(ast)
|
|
45
52
|
return nil unless ast
|
|
46
53
|
|
|
47
|
-
# Only set instance variables on first call (when they're provided)
|
|
48
|
-
# Recursive calls pass nil which shouldn't override the saved values
|
|
49
|
-
@source = source unless source.nil?
|
|
50
|
-
@include_source = include_source unless include_source.nil?
|
|
51
|
-
|
|
52
54
|
# Optimized: Hash is 90%+ of cases, check it first
|
|
53
55
|
case ast
|
|
54
56
|
when Hash
|
|
@@ -68,26 +70,27 @@ module Expressir
|
|
|
68
70
|
return result
|
|
69
71
|
end
|
|
70
72
|
|
|
71
|
-
# Slow path:
|
|
72
|
-
# Try
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
73
|
+
# Slow path: first-key builder returned nil in a multi-key
|
|
74
|
+
# hash. Try remaining keys for actual content. This handles
|
|
75
|
+
# grammar patterns like `element >> (op_comma >> element).repeat`
|
|
76
|
+
# which produce {:op_comma => ..., :element => {...}} where
|
|
77
|
+
# the first key is an operator separator.
|
|
78
|
+
# Previously gated by a hand-maintained OPERATOR_TOKENS set;
|
|
79
|
+
# now tries all remaining keys unconditionally (TODO.bugs/21).
|
|
80
|
+
ast.each_key do |key|
|
|
81
|
+
next if key == node_type
|
|
82
|
+
|
|
83
|
+
h_key = cached_snake_case(key)
|
|
84
|
+
h_builder = @register[h_key]
|
|
85
|
+
next unless h_builder
|
|
86
|
+
|
|
87
|
+
n_data = ast[key]
|
|
88
|
+
s_data = fast_convert_keys(n_data)
|
|
89
|
+
result = h_builder.call(s_data)
|
|
90
|
+
|
|
91
|
+
unless result.nil?
|
|
92
|
+
attach_source_info(result, n_data)
|
|
93
|
+
return result
|
|
91
94
|
end
|
|
92
95
|
end
|
|
93
96
|
else
|
|
@@ -105,12 +108,23 @@ module Expressir
|
|
|
105
108
|
end
|
|
106
109
|
end
|
|
107
110
|
|
|
108
|
-
# Build with remark attachment
|
|
111
|
+
# Build with remark attachment.
|
|
112
|
+
#
|
|
113
|
+
# Sets up a BuilderContext as a thread-local for the duration of the
|
|
114
|
+
# build so recursive build calls can read source/include_source via
|
|
115
|
+
# `Builder.source` / `Builder.include_source`. The previous context
|
|
116
|
+
# (if any) is restored on exit so nested builds are reentrant.
|
|
109
117
|
def build_with_remarks(ast, source: nil, include_source: nil)
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
|
|
113
|
-
|
|
118
|
+
# Trigger BuilderRegistry autoload so all AST node type handlers
|
|
119
|
+
# are registered before build() runs. The reference to
|
|
120
|
+
# BuilderRegistry resolves the autoload defined in express.rb.
|
|
121
|
+
BuilderRegistry
|
|
122
|
+
|
|
123
|
+
previous = Thread.current[CONTEXT_KEY]
|
|
124
|
+
Thread.current[CONTEXT_KEY] = BuilderContext.new(
|
|
125
|
+
source: source,
|
|
126
|
+
include_source: include_source,
|
|
127
|
+
)
|
|
114
128
|
|
|
115
129
|
result = build(ast)
|
|
116
130
|
|
|
@@ -122,6 +136,8 @@ module Expressir
|
|
|
122
136
|
end
|
|
123
137
|
|
|
124
138
|
result
|
|
139
|
+
ensure
|
|
140
|
+
Thread.current[CONTEXT_KEY] = previous
|
|
125
141
|
end
|
|
126
142
|
|
|
127
143
|
# Check if a builder is registered for a node type.
|
|
@@ -203,41 +219,11 @@ module Expressir
|
|
|
203
219
|
builder.call(data)
|
|
204
220
|
end
|
|
205
221
|
|
|
206
|
-
# Fast path for term nodes
|
|
207
|
-
def build_term(data)
|
|
208
|
-
build_node(:term, data)
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
# Fast path for factor nodes
|
|
212
|
-
def build_factor(data)
|
|
213
|
-
build_node(:factor, data)
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
# Fast path for simple_factor nodes
|
|
217
|
-
def build_simple_factor(data)
|
|
218
|
-
build_node(:simple_factor, data)
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
# Fast path for primary nodes
|
|
222
|
-
def build_primary(data)
|
|
223
|
-
build_node(:primary, data)
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
# Fast path for expression nodes
|
|
227
|
-
def build_expression(data)
|
|
228
|
-
build_node(:expression, data)
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
# Fast path for simple_expression nodes
|
|
232
|
-
def build_simple_expression(data)
|
|
233
|
-
build_node(:simple_expression, data)
|
|
234
|
-
end
|
|
235
|
-
|
|
236
222
|
private
|
|
237
223
|
|
|
238
224
|
# Cached snake_case conversion
|
|
239
225
|
def cached_snake_case(name)
|
|
240
|
-
|
|
226
|
+
snake_case_cache[name] ||= begin
|
|
241
227
|
str = name.to_s
|
|
242
228
|
# Check if already snake_case
|
|
243
229
|
if /^[a-z_]+$/.match?(str)
|
|
@@ -342,25 +328,27 @@ module Expressir
|
|
|
342
328
|
|
|
343
329
|
def extract_source_info(data)
|
|
344
330
|
return nil unless data
|
|
345
|
-
|
|
331
|
+
|
|
332
|
+
src = source
|
|
333
|
+
return nil unless src
|
|
346
334
|
|
|
347
335
|
slice = find_slice(data)
|
|
348
336
|
return nil unless slice
|
|
349
337
|
|
|
350
338
|
{
|
|
351
|
-
text:
|
|
339
|
+
text: src[slice.offset...(slice.offset + slice.length)]&.strip,
|
|
352
340
|
offset: slice.offset,
|
|
353
341
|
}
|
|
354
342
|
end
|
|
355
343
|
|
|
356
344
|
def attach_source_info(result, data)
|
|
357
|
-
return unless
|
|
345
|
+
return unless source && result.is_a?(Expressir::Model::ModelElement)
|
|
358
346
|
|
|
359
347
|
source_info = extract_source_info(data)
|
|
360
348
|
return unless source_info
|
|
361
349
|
|
|
362
350
|
result.source_offset = source_info[:offset]
|
|
363
|
-
result.source = source_info[:text] if
|
|
351
|
+
result.source = source_info[:text] if include_source
|
|
364
352
|
end
|
|
365
353
|
|
|
366
354
|
def find_slice(data, depth = 0)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module Express
|
|
5
|
+
# Value object carrying the per-build context that Builder needs to
|
|
6
|
+
# attach source info to model elements.
|
|
7
|
+
#
|
|
8
|
+
# Stored as a thread-local by Builder.build_with_remarks so that recursive
|
|
9
|
+
# build calls can read `Builder.source` / `Builder.include_source` without
|
|
10
|
+
# the module mutating class-level state. Thread-local means concurrent
|
|
11
|
+
# builds in different threads don't corrupt each other; the save/restore
|
|
12
|
+
# pattern in build_with_remarks means nested builds are reentrant.
|
|
13
|
+
class BuilderContext
|
|
14
|
+
attr_reader :source, :include_source
|
|
15
|
+
|
|
16
|
+
def initialize(source:, include_source:)
|
|
17
|
+
@source = source
|
|
18
|
+
@include_source = include_source
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -10,9 +10,9 @@ module Expressir
|
|
|
10
10
|
# Expression
|
|
11
11
|
def build_expression(ast_data)
|
|
12
12
|
left = if ast_data[:simple_expression]
|
|
13
|
-
Builder.
|
|
13
|
+
Builder.build_node(:simple_expression, ast_data[:simple_expression])
|
|
14
14
|
elsif ast_data[:logical_expression]
|
|
15
|
-
Builder.
|
|
15
|
+
Builder.build_node(:simple_expression, ast_data[:logical_expression][:simple_expression])
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
if ast_data[:rel_op_extended] && ast_data[:rhs]
|
|
@@ -29,18 +29,18 @@ module Expressir
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def build_logical_expression(ast_data)
|
|
32
|
-
Builder.
|
|
32
|
+
Builder.build_node(:simple_expression, ast_data[:simple_expression])
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def build_numeric_expression(ast_data)
|
|
36
|
-
Builder.
|
|
36
|
+
Builder.build_node(:simple_expression, ast_data[:simple_expression])
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
# Simple expression (addition/subtraction chain)
|
|
40
40
|
def build_simple_expression(ast_data)
|
|
41
41
|
return nil unless ast_data[:term]
|
|
42
42
|
|
|
43
|
-
term = Builder.
|
|
43
|
+
term = Builder.build_node(:term, ast_data[:term])
|
|
44
44
|
rhs = ast_data[:rhs]
|
|
45
45
|
|
|
46
46
|
return term if rhs.nil? || (rhs.is_a?(Array) && rhs.empty?)
|
|
@@ -60,7 +60,7 @@ module Expressir
|
|
|
60
60
|
op_data = item[:operator]
|
|
61
61
|
operators << extract_operator(op_data[:add_like_op]) if op_data
|
|
62
62
|
if item[:term]
|
|
63
|
-
operands << Builder.
|
|
63
|
+
operands << Builder.build_node(:term, item[:term])
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
66
|
|
|
@@ -71,7 +71,7 @@ module Expressir
|
|
|
71
71
|
def build_term(ast_data)
|
|
72
72
|
return nil unless ast_data[:factor]
|
|
73
73
|
|
|
74
|
-
factor = Builder.
|
|
74
|
+
factor = Builder.build_node(:factor, ast_data[:factor])
|
|
75
75
|
rhs = ast_data[:rhs]
|
|
76
76
|
|
|
77
77
|
return factor if rhs.nil? || (rhs.is_a?(Array) && rhs.empty?)
|
|
@@ -90,7 +90,7 @@ module Expressir
|
|
|
90
90
|
item = r[:item] || r
|
|
91
91
|
op_data = item[:multiplication_like_op] || item[:mul_like_op]
|
|
92
92
|
operators << extract_operator(op_data) if op_data
|
|
93
|
-
operands << Builder.
|
|
93
|
+
operands << Builder.build_node(:factor, item[:factor]) if item[:factor]
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
build_binary_expression(operands, operators)
|
|
@@ -99,14 +99,14 @@ module Expressir
|
|
|
99
99
|
def build_factor(ast_data)
|
|
100
100
|
return nil unless ast_data[:simple_factor]
|
|
101
101
|
|
|
102
|
-
Builder.
|
|
102
|
+
Builder.build_node(:simple_factor, ast_data[:simple_factor])
|
|
103
103
|
end
|
|
104
104
|
|
|
105
105
|
def build_simple_factor(ast_data)
|
|
106
106
|
return nil unless ast_data
|
|
107
107
|
|
|
108
108
|
if ast_data[:primary]
|
|
109
|
-
Builder.
|
|
109
|
+
Builder.build_node(:primary, ast_data[:primary])
|
|
110
110
|
elsif ast_data[:simple_factor_expression]
|
|
111
111
|
Builder.build_node(:simple_factor_expression,
|
|
112
112
|
ast_data[:simple_factor_expression])
|
|
@@ -116,7 +116,7 @@ module Expressir
|
|
|
116
116
|
elsif ast_data[:constant_factor]
|
|
117
117
|
Builder.build_node(:constant_factor, ast_data[:constant_factor])
|
|
118
118
|
elsif ast_data[:expression]
|
|
119
|
-
Builder.
|
|
119
|
+
Builder.build_node(:expression, ast_data[:expression])
|
|
120
120
|
elsif ast_data[:aggregate_initializer]
|
|
121
121
|
Builder.build_node(:aggregate_initializer,
|
|
122
122
|
ast_data[:aggregate_initializer])
|
|
@@ -137,21 +137,21 @@ module Expressir
|
|
|
137
137
|
|
|
138
138
|
def build_simple_factor_expression(ast_data)
|
|
139
139
|
if ast_data[:primary]
|
|
140
|
-
Builder.
|
|
140
|
+
Builder.build_node(:primary, ast_data[:primary])
|
|
141
141
|
elsif ast_data[:expression]
|
|
142
|
-
Builder.
|
|
142
|
+
Builder.build_node(:expression, ast_data[:expression])
|
|
143
143
|
end
|
|
144
144
|
end
|
|
145
145
|
|
|
146
146
|
def build_simple_factor_unary_expression(ast_data)
|
|
147
147
|
op = extract_unary_op(ast_data[:unary_op])
|
|
148
148
|
operand = if ast_data[:simple_factor]
|
|
149
|
-
Builder.
|
|
149
|
+
Builder.build_node(:simple_factor, ast_data[:simple_factor])
|
|
150
150
|
elsif ast_data[:simple_factor_expression]
|
|
151
151
|
Builder.build_node(:simple_factor_expression,
|
|
152
152
|
ast_data[:simple_factor_expression])
|
|
153
153
|
elsif ast_data[:primary]
|
|
154
|
-
Builder.
|
|
154
|
+
Builder.build_node(:primary, ast_data[:primary])
|
|
155
155
|
end
|
|
156
156
|
|
|
157
157
|
if op
|
|
@@ -181,7 +181,7 @@ module Expressir
|
|
|
181
181
|
elsif ast_data[:population]
|
|
182
182
|
build_population(ast_data[:population], ast_data[:qualifier])
|
|
183
183
|
elsif ast_data[:expression]
|
|
184
|
-
Builder.
|
|
184
|
+
Builder.build_node(:expression, ast_data[:expression])
|
|
185
185
|
end
|
|
186
186
|
end
|
|
187
187
|
|
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
require "digest"
|
|
1
2
|
require "zlib"
|
|
2
3
|
|
|
3
4
|
module Expressir
|
|
4
5
|
module Express
|
|
5
6
|
class Cache
|
|
7
|
+
# Format: magic + SHA-256 (raw, 32 bytes) + Zlib-deflated Marshal.dump
|
|
8
|
+
# of Model::Cache. The digest detects corruption; a corrupted cache is
|
|
9
|
+
# disposable — callers drop the file and regenerate. Marshal is only
|
|
10
|
+
# safe for trusted, locally generated cache files.
|
|
11
|
+
MAGIC = "EXPRC1".freeze
|
|
12
|
+
|
|
13
|
+
DIGEST_BYTES = Digest::SHA256.digest("").bytesize
|
|
14
|
+
|
|
6
15
|
# Save Express model into a cache file
|
|
7
16
|
# @param file [String] cache file path
|
|
8
17
|
# @param content [Model::ModelElement] Express model
|
|
@@ -11,7 +20,7 @@ module Expressir
|
|
|
11
20
|
# @return [nil]
|
|
12
21
|
def self.to_file(file, content, root_path: nil,
|
|
13
22
|
test_overwrite_version: nil)
|
|
14
|
-
version = test_overwrite_version || VERSION
|
|
23
|
+
version = test_overwrite_version || Expressir::Version::VERSION
|
|
15
24
|
|
|
16
25
|
cache = Model::Cache.new(
|
|
17
26
|
version: version,
|
|
@@ -19,10 +28,9 @@ test_overwrite_version: nil)
|
|
|
19
28
|
root_path: root_path,
|
|
20
29
|
)
|
|
21
30
|
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
data = Zlib::Deflate.deflate(Marshal.dump(cache), 1)
|
|
32
|
+
File.binwrite(file, "#{MAGIC}#{Digest::SHA256.digest(data)}#{data}")
|
|
24
33
|
|
|
25
|
-
File.binwrite(file, yaml_compressed)
|
|
26
34
|
nil
|
|
27
35
|
end
|
|
28
36
|
|
|
@@ -31,18 +39,37 @@ test_overwrite_version: nil)
|
|
|
31
39
|
# @param root_path [String] Express repository root path, to be prepended to Express file paths if loading a portable cache file
|
|
32
40
|
# @param test_overwrite_version [String] don't use, only for tests
|
|
33
41
|
# @return [Model::ModelElement] Express model
|
|
42
|
+
# @raise [Error::CacheCorruptedError] if the file is not a valid cache file or its digest does not match
|
|
43
|
+
# @raise [Error::CacheVersionMismatchError] if the cache was written by another Expressir version
|
|
34
44
|
def self.from_file(file, root_path: nil, test_overwrite_version: nil)
|
|
35
|
-
version = test_overwrite_version || VERSION
|
|
45
|
+
version = test_overwrite_version || Expressir::Version::VERSION
|
|
36
46
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
cache =
|
|
47
|
+
raw = File.binread(file)
|
|
48
|
+
data = Zlib::Inflate.inflate(validated_data(raw))
|
|
49
|
+
cache = Marshal.load(data) # rubocop:disable Security/MarshalLoad
|
|
40
50
|
|
|
41
51
|
if cache.version != version
|
|
42
52
|
raise Error::CacheVersionMismatchError.new(cache.version, version)
|
|
43
53
|
end
|
|
44
54
|
|
|
45
55
|
cache
|
|
56
|
+
rescue TypeError, ArgumentError, RangeError
|
|
57
|
+
raise Error::CacheCorruptedError
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.validated_data(raw)
|
|
61
|
+
header = MAGIC.bytesize + DIGEST_BYTES
|
|
62
|
+
unless raw.bytesize > header && raw.start_with?(MAGIC)
|
|
63
|
+
raise Error::CacheCorruptedError
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
digest = raw.byteslice(MAGIC.bytesize, DIGEST_BYTES)
|
|
67
|
+
data = raw.byteslice(header, raw.bytesize - header)
|
|
68
|
+
unless Digest::SHA256.digest(data) == digest
|
|
69
|
+
raise Error::CacheCorruptedError
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
data
|
|
46
73
|
end
|
|
47
74
|
end
|
|
48
75
|
end
|
|
@@ -38,6 +38,13 @@ module Expressir
|
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
# Error raised when a cache file is not a valid cache file or fails integrity validation
|
|
42
|
+
class CacheCorruptedError < ExpressError
|
|
43
|
+
def initialize(message = "Cache file is corrupted or not a valid Expressir cache file")
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
41
48
|
# Base class for visitor-related errors
|
|
42
49
|
class VisitorError < ExpressError; end
|
|
43
50
|
|