8000 Bugfix/rrel lookup multifile problem, issue #377 by goto40 · Pull Request #379 · textX/textX · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Bugfix/rrel lookup multifile problem, issue #377 #379

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Apr 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ please take a look at related PRs and issues and see if the change affects you.

## [Unreleased]

### Fixed

- Fixed RREL lookup in case of multi-meta models (some special cases were not
handled correctly; [#379]).

[#379]: https://github.com/textX/textX/pull/379


## [3.0.0] (released: 2022-03-20)

Expand Down
7 changes: 7 additions & 0 deletions tests/functional/test_scoping/rrel_multifile/Grammar.tx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Model: includes*=Include r*=R a*=A r*=R;
A: 'A' name=ID '{' ( a=A | p1=P1 )* '}';
R: 'R' a+=[A|FQN|+pm:a*][','];
P1: 'P1' a+=[A|FQN|+pm:..(..)*.a*][',']; // many times ".." and 1x "A"
FQN: ID ('.' ID)*;
Include: '#include' importURI=STRING;
Comment: /\/\/.*?$/;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Model: includes*=Include r*=R a*=A r*=R;
A: 'A' name=ID '{' ( a=A | p1=P1 )* '}';
R: 'R' a+=[A|FQN|+pm:a*][','];
P1: 'P1' a+=[A|FQN|+p:..(..)*.a*][',']; // many times ".." and 1x "A"
FQN: ID ('.' ID)*;
Include: '#include' importURI=STRING;
Comment: /\/\/.*?$/;
12 changes: 12 additions & 0 deletions tests/functional/test_scoping/rrel_multifile/included.model
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
A a1 {
A aa1 {
A aaa1 {}
A aab1 {}
}
}
A a2 {
A aa2 {}
}
A R {
A r2 {}
}
5 changes: 5 additions & 0 deletions tests/functional/test_scoping/rrel_multifile/main.model
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#include "included.model"

R a1.aa1.aaa1, a1.aa1.aab1, R, R.r2
R R
R a2.aa2
17 changes: 17 additions & 0 deletions tests/functional/test_scoping/rrel_multifile/navigation0.model
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
A a1 {
A aa1 {
A aaa1 {}
A aab1 {}
P1 aaa1
P1 aa1
P1 ab1
P1 a2
}
A ab1 {}
}
A a2 {
A aa2 {}
}
A R {
A r2 {}
}
31 changes: 31 additions & 0 deletions tests/functional/test_scoping/rrel_multifile/navigation1.model
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#include "navigation1_included.model"

// activate importURI (in any case, also for `GrammarMissingPlusM.tx`):
R a2.aa2
// ^^^
// This is important for `test_lookup_multifile_missing_flag_m`:
// Here, we want to make sure the "#include" command has an effect:
// - at the moment this happens only if any scope provider is active,
// which is an ImportURI provder. In case of RREL, a RREL
// reference grammar rule need to have +m flag and also the reference needs
// to be present in the model to trigger the load command
// (see model.py, look for `ModelLoader`).
// - once the model is loaded in the referenced test, we want to
// make sure that the P1 reference bellow from the included model is not
// found (because of the missing +m flag), but that this R reference above is
// still found as the `R` rule from the grammar contains +m flag.

A a1 {
A aa1 {
A aaa1 {}
A aab1 {}
P1 aaa1
P1 aa1
P1 ab1
P1 a2
}
A ab1 {}
}
A R {
A r2 {}
}
15 changes: 15 additions & 0 deletions tests/functional/test_scoping/rrel_multifile/navigation1_err.model
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//missing: #include "navigation1_included.model"
A a1 {
A aa1 {
A aaa1 {}
A aab1 {}
P1 aaa1
P1 aa1
P1 ab1
P1 a2
}
A ab1 {}
}
A R {
A r2 {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
A a2 {
A aa2 {}
}
43 changes: 43 additions & 0 deletions tests/functional/test_scoping/test_rrel.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,3 +483,46 @@ def test_rrel_with_fixed_string_in_navigation_with_scalars():
using myi32 = i32 # found via "default lookup"
using myFoo = Unknown # --> not found
''')


def test_lookup_multifile():
# same as test above, but with "+mp:" flag
from os.path import dirname, join
from textx import metamodel_from_file
this_folder = dirname(__file__)
mm = metamodel_from_file(join(this_folder, 'rrel_multifile', 'Grammar.tx'))

# "standard" multi-file usage
m = mm.model_from_file(join(this_folder, 'rrel_multifile', 'main.model'))
assert m is not None

# Rule "P1" employs a mandatory rrel path entry ".." (no multi-file)
m = mm.model_from_file(join(this_folder, 'rrel_multifile', 'navigation0.model'))
assert m is not None

# Rule "P1" employs a mandatory rrel path entry ".." (with multi-file)
# Note: "+pm:..(..)*.a*" works with the current impl (TODO: remove comment)
# TODO: in case of multi-files, do not start at the root of each model, but
# iterate over all models once the path reaches the model root (e.g. with .. in
# a navigation rrel node)...
m = mm.model_from_file(join(this_folder, 'rrel_multifile', 'navigation1.model'))
assert m is not None

# the next exammple is missing the include statement (leads to "Unknown object...")
with raises(TextXSemanticError, match=r'Unknown object'):
_ = mm.model_from_file(join(this_folder, 'rrel_multifile',
'navigation1_err.model'))


def test_lookup_multifile_missing_flag_m():
from os.path import dirname, join
from textx import metamodel_from_file
this_folder = dirname(__file__)

# the next exammple is missing the +m flag in the grammar
# (lookup across files disabled):
mmE = metamodel_from_file(join(this_folder, 'rrel_multifile',
'GrammarMissingPlusM.tx'))
with raises(TextXSemanticError, match=r'Unknown object'):
_ = mmE.model_from_file(join(this_folder, 'rrel_multifile',
'navigation1.model'))
6 changes: 6 additions & 0 deletions textx/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -833,11 +833,17 @@ def call_obj_processors(metamodel, model_obj,
if hasattr(model, '_tx_metamodel'):
assert hasattr(model, '_tx_model_params')

# Load all imported models (e.g. using importURI)
# based on the maker `ModelLoader` found in the
# defined scope providers:
for scope_provider in metamodel.scope_providers.values():
from textx.scoping import ModelLoader
if isinstance(scope_provider, ModelLoader):
scope_provider.load_models(model, encoding=encoding)

# Load all imported models based on the maker
# `ModelLoader` directly attached to model references
# (e.g. in case of RREL expressions defined in the grammar):
for crossref in parser._crossrefs:
crossref = crossref[2]
if crossref.scope_provider is not None:
Expand Down
95 changes: 68 additions & 27 deletions textx/scoping/rrel.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ def __init__(self, name, consume_name, fixed_name):
self.name = name
self.consume_name = consume_name
self.fixed_name = fixed_name
self.rrel_expression = None # is set after tree is built, req. for +m flag

def __repr__(self):
if self.fixed_name is not None:
Expand All @@ -160,42 +161,63 @@ def apply(self, obj, lookup_list, matched_path, first_element):
The object indicated by the navigation object,
Postponed, None, or a list (if a list has to be processed).
"""
assert self.rrel_expression is not None
from textx.scoping.tools import needs_to_be_resolved
from textx.scoping import Postponed
if first_element:
from textx import get_model
obj = get_model(obj)

start = [obj]
if not hasattr(obj, "parent"): # am I a root model node?
if self.rrel_expression.importURI:
if hasattr(obj, "_tx_model_repository"):
for m in obj._tx_model_repository.local_models:
start.append(m)
if obj._tx_metamodel.builtin_models:
for m in obj._tx_metamodel.builtin_models:
start.append(m)

if len(lookup_list) == 0 and self.consume_name:
return None, lookup_list, matched_path
if needs_to_be_resolved(obj, self.name):
return Postponed(), lookup_list, matched_path
if hasattr(obj, self.name):
target = getattr(obj, self.name)
if not self.consume_name and self.fixed_name is None:
return target, lookup_list, matched_path # return list
else:
if not isinstance(target, list):
target = [target]
if self.fixed_name is not None:
lst = list(filter(lambda x: hasattr(
x, "name") and getattr(
x, "name") == self.fixed_name, target))
if len(lst) > 0:
return lst[0], lookup_list, matched_path + [
lst[0]] # return obj
else:
return None, lookup_list, matched_path # return None

< 10000 /span>
def lookup(obj):
if needs_to_be_resolved(obj, self.name):
return Postponed(), lookup_list, matched_path
if hasattr(obj, self.name):
target = getattr(obj, self.name)
if not self.consume_name and self.fixed_name is None:
return target, lookup_list, matched_path # return list
else:
lst = list(filter(lambda x: hasattr(
x, "name") and getattr(
x, "name") == lookup_list[0], target))
if len(lst) > 0:
return lst[0], lookup_list[1:], matched_path + [
lst[0]] # return obj
if not isinstance(target, list):
target = [target]
if self.fixed_name is not None:
lst = list(filter(lambda x: hasattr(
x, "name") and getattr(
x, "name") == self.fixed_name, target))
if len(lst) > 0:
return lst[0], lookup_list, matched_path + [
lst[0]] # return obj
else:
return None, lookup_list, matched_path # return None
else:
return None, lookup_list, matched_path # return None
else:
return None, lookup_list, matched_path
lst = list(filter(lambda x: hasattr(
x, "name") and getattr(
x, "name") == lookup_list[0], target))
if len(lst) > 0:
return lst[0], lookup_list[1:], matched_path + [
lst[0]] # return obj
else:
return None, lookup_list, matched_path # return None
else:
return None, lookup_list, matched_path

for start_obj in start:
res, res_lookup_list, res_lookup_path = lookup(start_obj)
if (res):
return res, res_lookup_list, res_lookup_path

return None, lookup_list, matched_path


class RRELBrackets(RRELBase):
Expand Down Expand Up @@ -401,6 +423,18 @@ def __init__(self, seq, flags):
self.importURI = ('m' in flags)
self.use_proxy = ('p' in flags)

def prepare_tree(node):
if isinstance(node, RRELNavigation):
node.rrel_expression = self
if isinstance(node, RRELBase):
for c in node.__dict__.values():
if isinstance(c, list):
for e in c:
prepare_tree(e)
else:
prepare_tree(c)
prepare_tree(self.seq)

def __repr__(self):
if self.importURI:
return "+" + self.flags + ":" + str(self.seq)
Expand Down Expand Up @@ -652,6 +686,13 @@ def __init__(self, rrel_tree, split_string, use_proxy,
importURI_converter=importURI_converter,
importURI_to_scope_name=importURI_to_scope_name)

def __call__(self, obj, attr, obj_ref):
# override `__call__`in order to ignore the `default ImportURI`
# implementation: Here, we just need to call the normal
# scope resolution of the RREL provider (+the `ModelLoader`
# feature of the `ImportURI` implementation):
return self.scope_provider(obj, attr, obj_ref)

if isinstance(rrel_tree_or_string, string_types):
rrel_tree_or_string = parse(rrel_tree_or_string)

Expand Down
0