From 2389aaf8c754c78464f27cf480e22a41a89de1b0 Mon Sep 17 00:00:00 2001 From: Alexandru Croitor Date: Mon, 7 Oct 2019 08:38:08 +0200 Subject: pro2cmake: Handle qmake condition operator precedence Unfortunately qmake does not have operator precedence in conditions, and each sub-expression is simply evaluated left to right. So c1|c2:c3 is evaluated as (c1|c2):c3 and not c1|(c2:c3). To handle that in pro2cmake, wrap each condition sub-expression in parentheses. It's ugly, but there doesn't seem to be another way of handling it, because SymPy uses Python operator precedence for condition operators, and it's not possible to change the precendece. Fixes: QTBUG-78929 Change-Id: I6ab767c4243e3f2d0fea1c36cd004409faba3a53 Reviewed-by: Alexandru Croitor --- util/cmake/qmake_parser.py | 51 +++++++++++++++++++++- .../tests/data/condition_operator_precedence.pro | 11 +++++ util/cmake/tests/test_parsing.py | 14 ++++++ 3 files changed, 75 insertions(+), 1 deletion(-) mode change 100644 => 100755 util/cmake/qmake_parser.py create mode 100644 util/cmake/tests/data/condition_operator_precedence.pro (limited to 'util') diff --git a/util/cmake/qmake_parser.py b/util/cmake/qmake_parser.py old mode 100644 new mode 100755 index 836e9a4319..a6be81d6da --- a/util/cmake/qmake_parser.py +++ b/util/cmake/qmake_parser.py @@ -333,12 +333,61 @@ class QmakeParser: "ConditionWhiteSpace", pp.Suppress(pp.Optional(pp.White(" "))) ) + # Unfortunately qmake condition operators have no precedence, + # and are simply evaluated left to right. To emulate that, wrap + # each condition sub-expression in parentheses. + # So c1|c2:c3 is evaluated by qmake as (c1|c2):c3. + # The following variable keeps count on how many parentheses + # should be added to the beginning of the condition. Each + # condition sub-expression always gets an ")", and in the + # end the whole condition gets many "(". Note that instead + # inserting the actual parentheses, we insert special markers + # which get replaced in the end. + condition_parts_count = 0 + # Whitespace in the markers is important. Assumes the markers + # never appear in .pro files. + l_paren_marker = "_(_ " + r_paren_marker = " _)_" + + def handle_condition_part(condition_part_parse_result: pp.ParseResults) -> str: + condition_part_list = [*condition_part_parse_result] + nonlocal condition_parts_count + condition_parts_count += 1 + condition_part_joined = "".join(condition_part_list) + # Add ending parenthesis marker. The counterpart is added + # in handle_condition. + return f"{condition_part_joined}{r_paren_marker}" + + ConditionPart.setParseAction(handle_condition_part) ConditionRepeated = add_element( "ConditionRepeated", pp.ZeroOrMore(ConditionOp + ConditionWhiteSpace + ConditionPart) ) + def handle_condition(condition_parse_results: pp.ParseResults) -> str: + nonlocal condition_parts_count + prepended_parentheses = l_paren_marker * condition_parts_count + result = prepended_parentheses + " ".join(condition_parse_results).strip().replace( + ":", " && " + ).strip(" && ") + # If there are only 2 condition sub-expressions, there is no + # need for parentheses. + if condition_parts_count < 3: + result = result.replace(l_paren_marker, "") + result = result.replace(r_paren_marker, "") + result = result.strip(" ") + else: + result = result.replace(l_paren_marker, "( ") + result = result.replace(r_paren_marker, " )") + # Strip parentheses and spaces around the final + # condition. + result = result[1:-1] + result = result.strip(" ") + # Reset the parenthesis count for the next condition. + condition_parts_count = 0 + return result + Condition = add_element("Condition", pp.Combine(ConditionPart + ConditionRepeated)) - Condition.setParseAction(lambda x: " ".join(x).strip().replace(":", " && ").strip(" && ")) + Condition.setParseAction(handle_condition) # Weird thing like write_file(a)|error() where error() is the alternative condition # which happens to be a function call. In this case there is no scope, but our code expects diff --git a/util/cmake/tests/data/condition_operator_precedence.pro b/util/cmake/tests/data/condition_operator_precedence.pro new file mode 100644 index 0000000000..8af628404d --- /dev/null +++ b/util/cmake/tests/data/condition_operator_precedence.pro @@ -0,0 +1,11 @@ +a1|a2 { + DEFINES += d +} + +b1|b2:b3 { + DEFINES += d +} + +c1|c2:c3|c4 { + DEFINES += d +} diff --git a/util/cmake/tests/test_parsing.py b/util/cmake/tests/test_parsing.py index 02ca7f8ae4..bd784edd86 100755 --- a/util/cmake/tests/test_parsing.py +++ b/util/cmake/tests/test_parsing.py @@ -28,7 +28,9 @@ ############################################################################# import os +from pro2cmake import map_condition from qmake_parser import QmakeParser +from condition_simplifier import simplify_condition _tests_path = os.path.dirname(os.path.abspath(__file__)) @@ -352,3 +354,15 @@ def test_value_function(): assert target == 'Dummy' value = result[1]['value'] assert value[0] == '$$TARGET' + + +def test_condition_operator_precedence(): + result = parse_file(_tests_path + '/data/condition_operator_precedence.pro') + + def validate_simplify(input_str: str, expected: str) -> None: + output = simplify_condition(map_condition(input_str)) + assert output == expected + + validate_simplify(result[0]["condition"], "a1 OR a2") + validate_simplify(result[1]["condition"], "b3 AND (b1 OR b2)") + validate_simplify(result[2]["condition"], "c4 OR (c1 AND c3) OR (c2 AND c3)") -- cgit v1.2.1