Skip to content
Closed
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
11 changes: 1 addition & 10 deletions aas_core_codegen/golang/lib/_generate_verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,16 +116,7 @@ class RegexRenderer(parse_retree.Renderer):
def char_to_str_and_escape_or_encode_if_necessary(
self, node: parse_retree.Char, escaping: Mapping[str, str]
) -> List[Union[str, parse_tree.FormattedValue]]:
if not node.explicitly_encoded:
escaped = escaping.get(node.character, None)
if escaped is not None:
result: List[Union[str, parse_tree.FormattedValue]] = [escaped]
else:
result = [node.character]

return result

return [node.character]
return [escaping.get(node.character, node.character)]


_REGEX_RENDERER = RegexRenderer()
Expand Down
5 changes: 5 additions & 0 deletions aas_core_codegen/golang/lib/_generate_xml_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,11 @@ def _generate_read_element_dispatched() -> Stripped:
{II}return
{I}}}

{I}current, err = SkipEmptyTextWhitespaceAndComments(decoder, current)
{I}if err != nil {{
{II}return
{I}}}

{I}err = checkEndElementInNamespace(current, local, ns)
{I}if err != nil {{
{II}return
Expand Down
2 changes: 1 addition & 1 deletion aas_core_codegen/golang/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ def execute(context: run.Context, stdout: TextIO, stderr: TextIO) -> int:

if errors is not None:
run.write_error_report(
message=f"Failed to generate {rel_path} "
message=f"Failed to generate {rel_path.as_posix()} "
f"based on {context.model_path}",
errors=[
context.lineno_columner.error_message(error) for error in errors
Expand Down
162 changes: 13 additions & 149 deletions aas_core_codegen/golang/tests/_generate_xmlization_test_common_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@ def generate(repo_url: Stripped) -> str:
Stripped(
f"""\
import (
{I}"encoding/xml"
{I}"fmt"
{I}"os"
{I}"path/filepath"
{I}"regexp"
{I}"strings"
{I}"testing"
{I}ourtesting "{repo_url}/ourtesting"
{I}ourtypes "{repo_url}/types"
{I}ourxmlization "{repo_url}/xmlization"
)"""
),
Expand Down Expand Up @@ -86,166 +87,29 @@ def generate(repo_url: Stripped) -> str:
{II}return
{I}}}
{I}return
}}"""
),
Stripped(
"""\
// NOTE (mristin):
// Currently, Go does not support self-closing tags,
// see: https://github.com/golang/go/issues/21399.
// We apply the following hack to make the tags self-closing even if they are not.
// This is unsafe in general, but works OK for the limited set of test data that we are
// here dealing with.
//
// The code has been taken from: https://github.com/golang/go/issues/21399#issuecomment-1342730174"""
),
Stripped(
"""\
// NOTE (mristin):
// The opening tag, tag name plus an optional attribute list (*e.g.*,
// ``xmlns="..."`` on a root element), is captured in the first (and only)
// submatch group so that we do not lose any attributes when we force
// the tag to be self-closing below. The attribute list must not end with
// a slash, as we would otherwise match an already self-closing tag followed
// by the closing tag of its parent, *e.g.*, ``<someBytes /></something>``.
var emptyTagRe = regexp.MustCompile(`<(\\w+(?:\\s[^>]*[^/>])?)></\\w+>`)"""
),
Stripped(
f"""\
func forceSelfClosingTags(text string) string {{
{I}b := []byte(text)
{I}emptyTagIdxs := emptyTagRe.FindAllSubmatchIndex(b, -1)

{I}if len(emptyTagIdxs) == 0 {{
{II}return text
{I}}}

{I}// NOTE (mristin):
{I}// The matches are walked in order, and the pieces of `b` between them are
{I}// copied over as they are. Mind that `nb` starts out empty and that `b` is
{I}// only ever *read*: an earlier version appended into `b[0:idx[0]]`, which
{I}// writes into `b`'s own backing array, so a document with more than one
{I}// empty tag came out garbled -- every turn of the loop then indexed into
{I}// a `b` which the previous turn had already overwritten.
{I}var nb []byte
{I}previous := 0

{I}for _, idx := range emptyTagIdxs {{
{II}// Copy everything since the previous match, then the tag itself --
{II}// its name and its attributes -- as a self-closing one.
{II}nb = append(nb, b[previous:idx[0]]...)
{II}nb = append(nb, fmt.Sprintf("<%s/>", b[idx[2]:idx[3]])...)

{II}previous = idx[1]
{I}}}

{I}// Finally, copy everything after the last match.
{I}nb = append(nb, b[previous:]...)

{I}return string(nb)
}}"""
),
Stripped(
"""\
var whitespaceBetweenTagsRe = regexp.MustCompile(`>\\s+<`)"""
),
Stripped(
f"""\
// Remove all whitespace (including newlines, tabs, and spaces) between XML tags
// without parsing the XML.
func removeWhitespaceBetweenTags(text string) string {{
{I}return whitespaceBetweenTagsRe.ReplaceAllString(text, "><")
}}"""
),
Stripped(
"""\
var whitespaceBeforeAngleBracketsRe = regexp.MustCompile(`\\s+>`)
var whitespaceBeforeSelfClosingRe = regexp.MustCompile(`\\s+/>`)
var whitespaceAfterAngleBracketsRe = regexp.MustCompile(`<\\s+`)"""
),
Stripped(
f"""\
// Remove any whitespace before `>` and `/>` or after `<`
func removeWhitespaceWithinTags(text string) string {{
{I}result := text
{I}result = whitespaceBeforeAngleBracketsRe.ReplaceAllString(result, ">")
{I}result = whitespaceBeforeSelfClosingRe.ReplaceAllString(result, "/>")
{I}result = whitespaceAfterAngleBracketsRe.ReplaceAllString(result, "<")
{I}return result
}}"""
),
Stripped(
f"""\
// Assert that the serialization `other`, as XML document, equals the original
//
// XML document `that` read from the `source`.
// Assert that serializing and de-serializing preserves the original instance.
func assertSerializationEqualsDeserialization(
{I}t *testing.T,
{I}that string,
{I}that ourtypes.IClass,
{I}other string,
{I}source string,
) (ok bool) {{
{I}ok = true

{I}// Remove carriers to avoid problems between Windows, Posix and MacOS
{I}canonicalThat := strings.ReplaceAll(that, "\\r", "")
{I}canonicalOther := strings.ReplaceAll(other, "\\r", "")

{I}canonicalThat = strings.TrimSpace(canonicalThat)
{I}canonicalOther = strings.TrimSpace(canonicalOther)

{I}canonicalThat = forceSelfClosingTags(canonicalThat)
{I}canonicalOther = forceSelfClosingTags(canonicalOther)

{I}// NOTE (mristin):
{I}// The following hack is SUPER ugly and unsafe! However, it works. Given Go's
{I}// limited support for XML, we gave up on a safer approach :(. We tested
{I}// the following approaches before applying this hack:
{I}// * A round-trip over `encoding/xml`. Failed due to
{I}// https://github.com/golang/go/issues/13400.
{I}// * Using `aqwari.net/xml/xmltree`. Failed as the special characters in the
{I}// element content still has not been de-escaped or consistently escaped in
{I}// a round trip.

{I}canonicalThat = strings.ReplaceAll(canonicalThat, "'", "&#39;")
{I}canonicalOther = strings.ReplaceAll(canonicalOther, "'", "&#39;")

{I}canonicalThat = removeWhitespaceBetweenTags(canonicalThat)
{I}canonicalOther = removeWhitespaceBetweenTags(canonicalOther)

{I}canonicalThat = removeWhitespaceWithinTags(canonicalThat)
{I}canonicalOther = removeWhitespaceWithinTags(canonicalOther)

{I}thatLines := strings.Split(canonicalThat, "\\n")
{I}otherLines := strings.Split(canonicalOther, "\\n")

{I}if canonicalThat != canonicalOther {{
{II}b := new(strings.Builder)
{II}minLines := len(thatLines)
{II}if minLines > len(otherLines) {{
{III}minLines = len(otherLines)
{II}}}
{II}for i := 0; i < minLines; i++ {{
{III}if thatLines[i] == otherLines[i] {{
{IIII}b.WriteString(fmt.Sprintf(" %s\\n", thatLines[i]))
{III}}} else {{
{IIII}b.WriteString(fmt.Sprintf("ORIGINAL %s\\n", thatLines[i]))
{IIII}b.WriteString(fmt.Sprintf("SERIALIZED %s\\n", otherLines[i]))
{IIII}break
{III}}}
{II}}}
{I}decoder := xml.NewDecoder(strings.NewReader(other))
{I}roundTripped, err := ourxmlization.Unmarshal(decoder)
{I}if !assertNoDeserializationError(t, err, source) {{
{II}return false
{I}}}

{II}ok = false
{II}t.Fatalf(
{III}"The canonicalized XML serialization of the de-serialized instance "+
{IIII}"from %s does not equal the canonicalized original XML document:\\n"+
{IIII}"%s",
{III}source, b.String(),
{II})
{II}return
{I}if !ourtesting.DeepEqual(that, roundTripped) {{
{II}t.Fatalf("XML round-trip changed the instance from %s", source)
{II}return false
{I}}}

{I}return
{I}return true
}}"""
),
Stripped(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def _generate_for_cls(cls: intermediate.ConcreteClass) -> List[Stripped]:

{II}ok = assertSerializationEqualsDeserialization(
{III}t,
{III}text,
{III}deserialized,
{III}roundTrip,
{III}pth,
{II})
Expand Down
50 changes: 50 additions & 0 deletions aas_core_codegen/golang/transpilation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,56 @@ def transform_function_call(
for arg_node, arg in zip(node.args, args)
]

for i, (arg_node, argument) in enumerate(
zip(node.args, func_type.func.arguments)
):
source_type = intermediate_type_inference.beneath_optional(
self.type_map[arg_node]
)
target_type = intermediate.beneath_optional(argument.type_annotation)
if not (
isinstance(func_type.func, intermediate.TranspilableVerification)
and not argument.mutable
and isinstance(
source_type, intermediate_type_inference.ListTypeAnnotation
)
and isinstance(target_type, intermediate.ListTypeAnnotation)
and isinstance(
source_type.items, intermediate_type_inference.OurTypeAnnotation
)
and isinstance(target_type.items, intermediate.OurTypeAnnotation)
and isinstance(source_type.items.our_type, intermediate.Class)
and isinstance(target_type.items.our_type, intermediate.Class)
and source_type.items.our_type is not target_type.items.our_type
and source_type.items.our_type.is_subclass_of(
target_type.items.our_type
)
):
continue

# NOTE (mristin):
# Go slices are invariant even when their item interfaces inherit.
# We convert only mismatching read-only arguments, preserving nil.
source_slice = self._our_type_name(
golang_naming.interface_name(source_type.items.our_type.name)
)
target_slice = self._our_type_name(
golang_naming.interface_name(target_type.items.our_type.name)
)
args[i] = Stripped(
f"""\
func(items []{source_slice}) []{target_slice} {{
{I}if items == nil {{
{II}return nil
{I}}}
{I}result := make([]{target_slice}, len(items))
{I}for i, item := range items {{
{II}result[i] = item
{I}}}
{I}return result
}}({args[i]})"""
)

args_joined = ", ".join(args)

# Apply heuristic for breaking the lines
Expand Down
26 changes: 25 additions & 1 deletion dev/test_data/common_meta_models/list_of_classes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from enum import Enum
from typing import List
from typing import List, Optional, Sequence

from icontract import invariant

Expand All @@ -17,6 +17,30 @@ def __init__(self, name: str) -> None:
self.name = name


@verification
def items_are_some(items: Sequence[Abstract_item]) -> bool:
"""Check a read-only list of base-class instances."""
return all(isinstance(item, Some_item) for item in items)


@verification
def some_items_are_some(items: Sequence[Some_item]) -> bool:
"""Pass a list of descendants to a base-class list parameter."""
return items_are_some(items)


@verification
def optional_some_items_are_some(items: Optional[Sequence[Some_item]]) -> bool:
"""Preserve an optional descendant list passed to an optional parameter."""
return optional_items_are_some(items)


@verification
def optional_items_are_some(items: Optional[Sequence[Abstract_item]]) -> bool:
"""Check a possibly missing read-only list of base-class instances."""
return items is None or items_are_some(items)


class Another_item(Abstract_item):
serial_number: int

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,42 @@ class RecursiveVerification : public IVerification {
const std::shared_ptr<types::IClass>& instance_;
}; // class RecursiveVerification

// region Verification functions

/// \brief Check a read-only list of base-class instances.
bool ItemsAreSome(
const std::vector<
std::shared_ptr<types::IAbstractItem>
>& items
);

/// \brief Pass a list of descendants to a base-class list parameter.
bool SomeItemsAreSome(
const std::vector<
std::shared_ptr<types::ISomeItem>
>& items
);

/// \brief Preserve an optional descendant list passed to an optional parameter.
bool OptionalSomeItemsAreSome(
const common::optional<
std::vector<
std::shared_ptr<types::ISomeItem>
>
>& items
);

/// \brief Check a possibly missing read-only list of base-class instances.
bool OptionalItemsAreSome(
const common::optional<
std::vector<
std::shared_ptr<types::IAbstractItem>
>
>& items
);

// endregion Verification functions

} // namespace verification
/**@}*/

Expand Down
Loading
Loading