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 <alexandru.croitor@qt.io>
This commit is contained in:
Alexandru Croitor 2019-10-07 08:38:08 +02:00 committed by Joerg Bornemann
parent 6708fad936
commit 2389aaf8c7
3 changed files with 75 additions and 1 deletions

51
util/cmake/qmake_parser.py Normal file → Executable file
View File

@ -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

View File

@ -0,0 +1,11 @@
a1|a2 {
DEFINES += d
}
b1|b2:b3 {
DEFINES += d
}
c1|c2:c3|c4 {
DEFINES += d
}

View File

@ -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)")