expressir 2.4.12 → 2.4.14
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/lib/expressir/commands/parity_inputs.rb +86 -45
- data/lib/expressir/express/checker.rb +99 -6
- data/lib/expressir/express/shtolo.rb +38 -15
- data/lib/expressir/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ad3043a3dd1ac401e841249e3e081a7b3b6b3c3da99f6a9dbc136a127619ba49
|
|
4
|
+
data.tar.gz: '085e25b6e9b97d53fc5207aa651b37b080557626c6971b60029b98749ff4db82'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e92cca737def4e8432f30b98162262f83df7d81c719c7a1dd916173f65ff788c1145843e46131bfe7da5ef97e76c00a155215f7c4893befe7748601711438d93
|
|
7
|
+
data.tar.gz: 88d3f1008222838323a843a1b9a7117bf1b09465fa453de4bfd21868137b16439a680165e527555099ae67884cb8a77c8a2068e92b113f34408444c314482583
|
|
@@ -5,58 +5,92 @@ module Expressir
|
|
|
5
5
|
# Common parser/discovery helpers shared by parity CLI commands
|
|
6
6
|
# (expand, flatten, check) — they all need: list of files in the
|
|
7
7
|
# interface closure of a root file, and the root schema parsed.
|
|
8
|
+
#
|
|
9
|
+
# Schema resolution order (per the ELF schema manifest directive):
|
|
10
|
+
# 1. `--manifest PATH` — an ELF schema manifest explicitly defines
|
|
11
|
+
# where each schema lives; this is the primary mode.
|
|
12
|
+
# 2. `--stepmod DIR` — explicit opt-in to the STEPmod directory
|
|
13
|
+
# convention (`schemas/**/<name>.exp`) as the fallback.
|
|
14
|
+
# 3. Otherwise the file's own directory is used; a lookup failure
|
|
15
|
+
# warns and points at the two explicit modes.
|
|
8
16
|
module ParityInputs
|
|
9
17
|
module_function
|
|
10
18
|
|
|
11
|
-
# Resolve the transitive interface closure of +root_path+.
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
19
|
+
# Resolve the transitive interface closure of +root_path+. The queue
|
|
20
|
+
# holds paths and `seen` guards by schema name: mutual USE FROM
|
|
21
|
+
# cycles must terminate (this loop once span rounds forever on them,
|
|
22
|
+
# re-reading the same files).
|
|
15
23
|
def closure_paths(root_path, manifest: nil, stepmod: nil)
|
|
16
|
-
index = schema_index(root_path)
|
|
24
|
+
index = schema_index(root_path, manifest: manifest, stepmod: stepmod)
|
|
17
25
|
seen = { File.basename(root_path, ".exp").downcase => root_path }
|
|
18
|
-
|
|
26
|
+
missing = []
|
|
27
|
+
queue = [root_path]
|
|
19
28
|
until queue.empty?
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
next if seen.key?(name) && seen[name] != root_path
|
|
29
|
+
closure_names(File.read(queue.shift)).each do |dep|
|
|
30
|
+
name = dep.downcase
|
|
31
|
+
next if seen.key?(name)
|
|
24
32
|
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
path = index&.[](name)
|
|
34
|
+
unless path
|
|
35
|
+
missing << name
|
|
36
|
+
seen[name] = nil
|
|
37
|
+
next
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
seen[name] = path
|
|
41
|
+
queue << path
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
unless missing.empty?
|
|
45
|
+
warn "expressir: schema(s) not found: #{missing.uniq.join(', ')}" \
|
|
46
|
+
"#{resolver_hint(manifest, stepmod)}"
|
|
27
47
|
end
|
|
28
|
-
seen.values.uniq
|
|
48
|
+
seen.values.compact.uniq
|
|
29
49
|
end
|
|
30
50
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
parent = File.dirname(dir)
|
|
42
|
-
break if parent == dir
|
|
51
|
+
def resolver_hint(manifest, stepmod)
|
|
52
|
+
if manifest
|
|
53
|
+
" (not in schema manifest #{manifest})"
|
|
54
|
+
elsif stepmod
|
|
55
|
+
" (not found under STEPmod root #{stepmod})"
|
|
56
|
+
else
|
|
57
|
+
" - provide --manifest PATH (ELF schema manifest) or " \
|
|
58
|
+
"--stepmod DIR to resolve them"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
43
61
|
|
|
44
|
-
|
|
62
|
+
# Schema-name → path index from the ELF schema manifest.
|
|
63
|
+
def manifest_index(manifest_path)
|
|
64
|
+
manifest = Expressir::SchemaManifest.from_file(manifest_path)
|
|
65
|
+
manifest.schemas.to_h do |entry|
|
|
66
|
+
[entry.id.downcase, entry.path]
|
|
45
67
|
end
|
|
46
|
-
|
|
68
|
+
rescue StandardError => e
|
|
69
|
+
warn "expressir: could not load schema manifest #{manifest_path}: #{e.message}"
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
47
72
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
73
|
+
# Schema-name → path index over a STEPmod checkout root (the
|
|
74
|
+
# directory holding `schemas/`, or that directory itself).
|
|
75
|
+
def stepmod_index(stepmod_dir)
|
|
76
|
+
root = File.join(File.expand_path(stepmod_dir), "schemas")
|
|
77
|
+
root = File.expand_path(stepmod_dir) unless File.directory?(root)
|
|
78
|
+
h = {}
|
|
79
|
+
Find.find(root) do |path|
|
|
80
|
+
next unless path.end_with?(".exp")
|
|
53
81
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
end
|
|
58
|
-
h
|
|
82
|
+
declared = File.read(path)[/\bSCHEMA\s+(\w+)/i, 1]
|
|
83
|
+
h[declared.downcase] = path if declared
|
|
84
|
+
h[File.basename(path, ".exp").downcase] ||= path
|
|
59
85
|
end
|
|
86
|
+
h
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def schema_index(root_path, manifest: nil, stepmod: nil)
|
|
90
|
+
return manifest_index(manifest) if manifest
|
|
91
|
+
return stepmod_index(stepmod) if stepmod
|
|
92
|
+
|
|
93
|
+
same_dir_index(root_path)
|
|
60
94
|
end
|
|
61
95
|
|
|
62
96
|
def same_dir_index(root_path)
|
|
@@ -66,19 +100,26 @@ module Expressir
|
|
|
66
100
|
end
|
|
67
101
|
end
|
|
68
102
|
|
|
103
|
+
# Interface names declared by +source+. Remark text is stripped
|
|
104
|
+
# first: prose like "use from the main schema" must not spawn
|
|
105
|
+
# phantom dependencies.
|
|
69
106
|
def closure_names(source)
|
|
70
|
-
source.
|
|
71
|
-
|
|
107
|
+
code = source.gsub(/--[^\n]*/, "").gsub(/\(\*.*?\*\)/m, "")
|
|
108
|
+
code.scan(/\bUSE\s+FROM\s+(\w+)/i).flatten.uniq +
|
|
109
|
+
code.scan(/\bREFERENCE\s+FROM\s+(\w+)/i).flatten.uniq
|
|
72
110
|
end
|
|
73
111
|
|
|
74
|
-
# manifest:/stepmod: are accepted for the ELF-manifest resolver
|
|
75
|
-
# contract; the directory-index fallback ignores them until that
|
|
76
|
-
# resolver lands.
|
|
77
112
|
def root_schema(root_path, manifest: nil, stepmod: nil)
|
|
78
113
|
name = File.read(root_path)[/\bSCHEMA\s+(\w+)/, 1]
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
114
|
+
# skip_references: longform generation and checking run their own
|
|
115
|
+
# resolution; resolving every reference of a large closure here made
|
|
116
|
+
# flatten crawl (minutes vs seconds).
|
|
117
|
+
repo = Expressir::Express::Parser.from_files(
|
|
118
|
+
closure_paths(root_path, manifest: manifest, stepmod: stepmod),
|
|
119
|
+
skip_references: true,
|
|
120
|
+
max_processes: 1,
|
|
121
|
+
)
|
|
122
|
+
[repo.schemas.find { |s| s.id&.downcase == name&.downcase }, repo]
|
|
82
123
|
end
|
|
83
124
|
end
|
|
84
125
|
end
|
|
@@ -97,6 +97,11 @@ module Expressir
|
|
|
97
97
|
check_where_rules(schema, t, t.where_rules)
|
|
98
98
|
end
|
|
99
99
|
schema.rules.each { |r| check_where_rules(schema, r, r.where_rules) }
|
|
100
|
+
schema.entities.each do |entity|
|
|
101
|
+
check_aggregate_bounds(schema, entity)
|
|
102
|
+
check_attrib_name_fun(schema, entity)
|
|
103
|
+
end
|
|
104
|
+
check_string_references(schema)
|
|
100
105
|
check_unresolved_refs(schema)
|
|
101
106
|
end
|
|
102
107
|
|
|
@@ -307,20 +312,32 @@ module Expressir
|
|
|
307
312
|
|
|
308
313
|
Array(type.underlying_type.items).each do |item|
|
|
309
314
|
next if item.id.nil?
|
|
310
|
-
next if
|
|
311
|
-
next if find_entity(schema, item.id).nil? # unresolved handled elsewhere
|
|
315
|
+
next if select_item_resolves?(schema, item.id)
|
|
312
316
|
|
|
313
317
|
note!(:check_select_named_type, :error, schema,
|
|
314
|
-
"TYPE #{type.id}
|
|
315
|
-
"
|
|
318
|
+
"In the SELECT TYPE #{type.id}, '#{item.id}' does not " \
|
|
319
|
+
"name an existing TYPE or ENTITY", type)
|
|
316
320
|
end
|
|
317
321
|
end
|
|
318
322
|
|
|
319
323
|
def find_type(schema, id)
|
|
320
324
|
key = id.safe_downcase
|
|
321
|
-
|
|
325
|
+
schema.types.find { |t| t.id.safe_downcase == key }
|
|
326
|
+
end
|
|
322
327
|
|
|
323
|
-
|
|
328
|
+
# ISO 10303-11 rule 258: select_list items are named_types —
|
|
329
|
+
# entity_ref | type_ref. The item is fine when it names either,
|
|
330
|
+
# in this schema or any repository schema (interface-visible).
|
|
331
|
+
def select_item_resolves?(schema, id)
|
|
332
|
+
return true if find_entity(schema, id)
|
|
333
|
+
return true if schema.types.to_a.any? { |t| t.id.safe_downcase == id.safe_downcase }
|
|
334
|
+
|
|
335
|
+
@by_name.each_value.any? do |group|
|
|
336
|
+
group.any? do |other|
|
|
337
|
+
other.types.to_a.any? { |t| t.id.safe_downcase == id.safe_downcase } ||
|
|
338
|
+
other.entities.to_a.any? { |e| e.id.safe_downcase == id.safe_downcase }
|
|
339
|
+
end
|
|
340
|
+
end
|
|
324
341
|
end
|
|
325
342
|
|
|
326
343
|
# eeng declared-kind probe: :type when a TYPE of that name is
|
|
@@ -330,6 +347,82 @@ module Expressir
|
|
|
330
347
|
:type if find_type(schema, id)
|
|
331
348
|
end
|
|
332
349
|
|
|
350
|
+
# eeng check-agg-type: a two-bound aggregate whose lower bound
|
|
351
|
+
# exceeds its upper bound is invalid.
|
|
352
|
+
def check_aggregate_bounds(schema, entity)
|
|
353
|
+
entity.attributes.to_a.each do |attr|
|
|
354
|
+
type = attr.type
|
|
355
|
+
next unless type.respond_to?(:bound1) && type.respond_to?(:bound2)
|
|
356
|
+
|
|
357
|
+
lower = integer_bound(type.bound1)
|
|
358
|
+
upper = integer_bound(type.bound2)
|
|
359
|
+
next if lower.nil? || upper.nil?
|
|
360
|
+
next if lower <= upper
|
|
361
|
+
|
|
362
|
+
note!(:check_agg_type, :error, schema,
|
|
363
|
+
"ENTITY #{entity.id}: aggregate '#{attr.id}' bounds " \
|
|
364
|
+
"[#{lower}:#{upper}] are inverted", attr)
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def integer_bound(bound)
|
|
369
|
+
return nil unless bound.is_a?(Model::Literals::Integer)
|
|
370
|
+
|
|
371
|
+
bound.value.to_i
|
|
372
|
+
rescue StandardError
|
|
373
|
+
nil
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# eeng check-attrib-name-fun: an attribute sharing its name with
|
|
377
|
+
# a function of the same schema can never be referenced cleanly.
|
|
378
|
+
def check_attrib_name_fun(schema, entity)
|
|
379
|
+
funs = schema.functions.to_a.map { |f| f.id.safe_downcase }
|
|
380
|
+
entity.attributes.to_a.each do |attr|
|
|
381
|
+
next unless attr.id && funs.include?(attr.id.safe_downcase)
|
|
382
|
+
|
|
383
|
+
note!(:check_attrib_name_fun, :warning, schema,
|
|
384
|
+
"ENTITY #{entity.id}: attribute '#{attr.id}' shadows a " \
|
|
385
|
+
"function of the same name", attr)
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# eeng check-string-no-entity: string literals of the shape
|
|
390
|
+
# 'SCHEMA.ITEM' whose SCHEMA is known in the repository but whose
|
|
391
|
+
# ITEM is not declared in it (the TYPEOF idiom).
|
|
392
|
+
def check_string_references(schema)
|
|
393
|
+
known = @by_name
|
|
394
|
+
each_string(schema) do |literal, value|
|
|
395
|
+
match = /\A(\w+)\.(\w+)\z/.match(value)
|
|
396
|
+
next unless match
|
|
397
|
+
|
|
398
|
+
schema_id = match[1].safe_downcase
|
|
399
|
+
item_id = match[2].safe_downcase
|
|
400
|
+
target = known[schema_id]&.find { |s| s.id.safe_downcase == schema_id }
|
|
401
|
+
next unless target
|
|
402
|
+
next if Expressir::Express::SelfSchemaReference.declared?(target, item_id)
|
|
403
|
+
|
|
404
|
+
note!(:check_string_no_entity, :warning, schema,
|
|
405
|
+
"string '#{value}' references '#{item_id}' which is not " \
|
|
406
|
+
"declared in schema '#{target.id}'", literal)
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def each_string(node, &)
|
|
411
|
+
case node
|
|
412
|
+
when Model::Literals::String then yield node, node.value
|
|
413
|
+
when Model::ModelElement
|
|
414
|
+
node.class.attributes.each_key do |attr|
|
|
415
|
+
next if Model::ModelElement::SKIP_ATTRIBUTES.include?(attr) || attr == :parent
|
|
416
|
+
|
|
417
|
+
value = node.public_send(attr)
|
|
418
|
+
case value
|
|
419
|
+
when Array then value.each { |item| each_string(item, &) }
|
|
420
|
+
when Model::ModelElement then each_string(value, &)
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
333
426
|
def check_where_rules(schema, owner, rules)
|
|
334
427
|
Array(rules).each do |wr|
|
|
335
428
|
next if wr.id.nil? || wr.id.match?(WHERE_LABEL)
|
|
@@ -351,15 +351,22 @@ module Expressir
|
|
|
351
351
|
end
|
|
352
352
|
end
|
|
353
353
|
|
|
354
|
-
# Every identifier a
|
|
355
|
-
#
|
|
354
|
+
# Every identifier a call site names, across the artifact's
|
|
355
|
+
# surviving declarations: FunctionCall for functions and
|
|
356
|
+
# ProcedureCall statements for procedures. (A zero-argument
|
|
357
|
+
# function reference written bare is not represented as a
|
|
358
|
+
# FunctionCall by the parser and is counted via any SimpleReference
|
|
359
|
+
# miss — accepted v1 blind spot, see tranche notes.)
|
|
356
360
|
def called_ids(schema)
|
|
357
361
|
called = {}
|
|
358
362
|
each_node(schema) do |node|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
+
id = nil
|
|
364
|
+
case node
|
|
365
|
+
when Expressir::Model::Expressions::FunctionCall
|
|
366
|
+
id = node.function.is_a?(String) ? node.function : node.function&.id
|
|
367
|
+
when Expressir::Model::Statements::ProcedureCall
|
|
368
|
+
id = node.procedure.is_a?(String) ? node.procedure : node.procedure&.id
|
|
369
|
+
end
|
|
363
370
|
called[id.safe_downcase] = true if id
|
|
364
371
|
end
|
|
365
372
|
called
|
|
@@ -392,19 +399,28 @@ module Expressir
|
|
|
392
399
|
def reduce_supertype_expression(expr, visible_ids)
|
|
393
400
|
case expr
|
|
394
401
|
when Model::SupertypeExpressions::OneofSupertypeExpression
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
id.nil? || visible_ids.key?(id.safe_downcase)
|
|
402
|
+
reduced = expr.operands.filter_map do |operand|
|
|
403
|
+
reduce_supertype_expression(operand, visible_ids)
|
|
398
404
|
end
|
|
399
|
-
return nil if
|
|
405
|
+
return nil if reduced.empty?
|
|
400
406
|
|
|
401
|
-
|
|
407
|
+
reduced.one? ? reduced.first : reset_oneof(expr, reduced)
|
|
402
408
|
when Model::SupertypeExpressions::BinarySupertypeExpression
|
|
403
|
-
left = reduce_supertype_expression(expr.
|
|
404
|
-
right = reduce_supertype_expression(expr.
|
|
405
|
-
|
|
409
|
+
left = reduce_supertype_expression(expr.operand1, visible_ids)
|
|
410
|
+
right = reduce_supertype_expression(expr.operand2, visible_ids)
|
|
411
|
+
if left.nil?
|
|
412
|
+
return right
|
|
413
|
+
end
|
|
414
|
+
if right.nil?
|
|
415
|
+
return left
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
rebuild_binary(expr, left, right)
|
|
419
|
+
when Model::References::SimpleReference
|
|
420
|
+
id = expr.id
|
|
421
|
+
return nil if id && !visible_ids.key?(id.safe_downcase)
|
|
406
422
|
|
|
407
|
-
|
|
423
|
+
expr
|
|
408
424
|
else
|
|
409
425
|
expr
|
|
410
426
|
end
|
|
@@ -416,6 +432,13 @@ module Expressir
|
|
|
416
432
|
copy
|
|
417
433
|
end
|
|
418
434
|
|
|
435
|
+
def rebuild_binary(expr, left, right)
|
|
436
|
+
copy = deep_copy(expr)
|
|
437
|
+
copy.operand1 = left
|
|
438
|
+
copy.operand2 = right
|
|
439
|
+
copy
|
|
440
|
+
end
|
|
441
|
+
|
|
419
442
|
def resolve_extensible_enumerations!(schema)
|
|
420
443
|
schema.types.select do |t|
|
|
421
444
|
t.underlying_type.is_a?(Model::DataTypes::Enumeration) && t.underlying_type.extensible
|
data/lib/expressir/version.rb
CHANGED