interpreterbase: warn on redundant Meson version checks

We might encounter a Meson version check that always evaluates to true
or to false:

    project('t', 'c', meson_version: '>=0.60.0')
    if meson.version().version_compare('>=0.55.0')
      v = 1
    endif
    if meson.version().version_compare('<0.60.0')
      v = 2
    endif

Print warnings in such cases.

Co-authored-by: Benjamin Gilbert <bgilbert@backtick.net>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
This commit is contained in:
Paolo BonziniandBenjamin Gilbert authored and Dylan Baker committed 2026-06-02 11:05:24 -07:00
1 parent 0bfffde31b
commit b108299627
5 files changed
+112 -8

No files matched your search

+12 -8
View File
@@ -311,15 +311,19 @@ class InterpreterBase:
res = result.operator_call(MesonOperator.BOOL, None)
if not isinstance(res, bool):
raise InvalidCode(f'If clause {result!r} does not evaluate to true or false.')
if res:
prev_meson_version = mesonlib.project_meson_versions[self.subproject]
if self.tmp_meson_version:
mesonlib.project_meson_versions[self.subproject] = self.tmp_meson_version
try:
prev_meson_version = mesonlib.project_meson_versions[self.subproject]
if self.tmp_meson_version and isinstance(prev_meson_version, mesonlib.Range):
always = prev_meson_version.always(self.tmp_meson_version)
if always is not None:
mlog.warning(f"Conditional on version '{self.tmp_meson_version}' always evaluates to {str(always).lower()}",
location=self.current_node)
mesonlib.project_meson_versions[self.subproject] = prev_meson_version.intersect(self.tmp_meson_version)
try:
if res:
self.evaluate_codeblock(i.block)
finally:
mesonlib.project_meson_versions[self.subproject] = prev_meson_version
return None
return None
finally:
mesonlib.project_meson_versions[self.subproject] = prev_meson_version
if not isinstance(node.elseblock, mparser.EmptyNode):
self.evaluate_codeblock(node.elseblock.block)
return None
+12
View File
@@ -1095,6 +1095,18 @@ class Range(T.Generic[_V]):
result.__post_init__()
return result
def always(self, inner: Range[_V]) -> T.Optional[bool]:
"""Check if inner is always true or always false given self.
Returns True if inner is always satisfied by any value in self,
False if no value in self satisfies inner, None if indeterminate."""
narrowed = self.intersect(inner)
if narrowed.is_empty:
return False
if narrowed == self:
return True
return None
# note that Range is immutable, so no need to have Range() | None
def version_check_to_range(checks: T.List[str], start: Range[Version] = Range()) -> Range[Version]:
@@ -0,0 +1,33 @@
# written by Benjamin Gilbert <bgilbert@backtick.net>
project('t', 'c', meson_version: '>=0.60.0')
if meson.version().version_compare('>=0.55.0')
v = 1
endif
if meson.version().version_compare('<0.60.0')
v = 2
endif
# narrowing: <0.60 is indeterminate, but >=0.40 is always true within >=0.50, <0.60
if meson.version().version_compare('<99.0')
if meson.version().version_compare('>=0.40')
v = 3
endif
endif
# <= vs <, should not warn
if meson.version().version_compare('<=99.0')
if meson.version().version_compare('<99.0')
v = 4
endif
endif
# hide warnings about multiple args to version_compare()
if meson.version().version_compare('>1.10.0')
if meson.version().version_compare('<99.0')
if meson.version().version_compare('<99.0', '>=0.40')
v = 5
endif
if meson.version().version_compare('>=0.40', '<99.0')
v = 6
endif
endif
endif
@@ -0,0 +1,19 @@
{
"stdout": [
{
"line": "test cases/common/295 redundant version check/meson.build:3: WARNING: Conditional on version '>= 0.55.0' always evaluates to true"
},
{
"line": "test cases/common/295 redundant version check/meson.build:6: WARNING: Conditional on version '< 0.60.0' always evaluates to false"
},
{
"line": "test cases/common/295 redundant version check/meson.build:11: WARNING: Conditional on version '>= 0.40' always evaluates to true"
},
{
"line": "test cases/common/295 redundant version check/meson.build:26: WARNING: Conditional on version '>= 0.40, < 99.0' always evaluates to true"
},
{
"line": "test cases/common/295 redundant version check/meson.build:29: WARNING: Conditional on version '>= 0.40, < 99.0' always evaluates to true"
}
]
}
+36
View File
@@ -292,6 +292,42 @@ class VersionComparisonTests(unittest.TestCase):
r = a.intersect(b)
self.assertTrue(r.is_empty)
def test_range_always(self):
"""Range.always checks if inner is always true/false given outer."""
outer = Range(min=5, min_eq=True, max=10, max_eq=True)
unbounded = Range()
empty = Range(min=7, max=3, min_eq=True, max_eq=True)
# inner fully contains outer => true
self.assertTrue(outer.always(Range(min=3, min_eq=True)))
self.assertTrue(outer.always(Range(max=12, max_eq=True)))
self.assertTrue(outer.always(unbounded))
# inner same as outer => true
self.assertTrue(outer.always(Range(min=5, min_eq=True, max=10, max_eq=True)))
# inner doesn't overlap outer => false
self.assertFalse(outer.always(Range(min=11, min_eq=True)))
self.assertFalse(outer.always(Range(max=4, max_eq=True)))
# inner partially overlaps => indeterminate
self.assertIsNone(outer.always(Range(min=7, min_eq=True)))
self.assertIsNone(outer.always(Range(max=8, max_eq=True)))
# exclusive boundaries count as a partial overlap
self.assertIsNone(outer.always(Range(min=5, min_eq=False)))
self.assertIsNone(outer.always(Range(max=10, max_eq=False)))
# outer is unbounded
self.assertIsNone(unbounded.always(Range(min=5, min_eq=True)))
self.assertTrue(unbounded.always(unbounded))
self.assertFalse(unbounded.always(empty))
# outer is empty => always false (intersection is empty)
self.assertFalse(empty.always(Range(min=5, min_eq=True)))
self.assertFalse(empty.always(unbounded))
self.assertFalse(empty.always(empty))
def test_range_str(self):
"""Test Range.__str__."""
self.assertEqual(str(Range()), '(any)')