From 8096a9a46299972755c72eba27c5833550dd75a7 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Tue, 16 Jun 2026 10:35:31 -0400 Subject: [PATCH 1/2] feat(version-scanner): refine python version checks and document boundary logic --- scripts/version_scanner/regex_config.yaml | 46 +++++++++++++++++-- .../tests/unit/test_version_scanner.py | 9 +++- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/scripts/version_scanner/regex_config.yaml b/scripts/version_scanner/regex_config.yaml index 95e62fe002aa..312e7c67d7f0 100644 --- a/scripts/version_scanner/regex_config.yaml +++ b/scripts/version_scanner/regex_config.yaml @@ -17,20 +17,23 @@ rules: examples: - "python_requires = '==3.7'" - "python_requires = '>=3.7'" + - "python_requires = '>=3.7.0'" - "python_requires = '<=3.7'" - "python_requires = '>3.6'" - "python_requires = '<3.8'" rules: - | - python_requires\s*=\s*['"]==3\.{minor}['"] + python_requires\s*=\s*['"]==3\.{minor}(?:\.\d+)?['"] - | - python_requires\s*=\s*['"]>=3\.{minor}['"] + python_requires\s*=\s*['"]>=3\.{minor}(?:\.\d+)?['"] - | - python_requires\s*=\s*['"]<=3\.{minor}['"] + python_requires\s*=\s*['"]<=3\.{minor}(?:\.\d+)?['"] + # Matches >3.6 (equivalent to >=3.7) - | - python_requires\s*=\s*['"]>3\.{minor_minus_one}['"] + python_requires\s*=\s*['"]>3\.{minor_minus_one}(?:\.\d+)?['"] + # Matches <3.8 (equivalent to <=3.7) - | - python_requires\s*=\s*['"]<3\.{minor_plus_one}['"] + python_requires\s*=\s*['"]<3\.{minor_plus_one}(?:\.\d+)?['"] - name: sys_version_info description: Finds sys.version_info checks in code. @@ -46,6 +49,11 @@ rules: - "sys.version_info.minor <= 7" - "sys.version_info.minor > 6" - "sys.version_info.minor < 8" + - "sys.version_info[1] == 7" + - "sys.version_info[1] >= 7" + - "sys.version_info[1] <= 7" + - "sys.version_info[1] > 6" + - "sys.version_info[1] < 8" rules: - | sys\.version_info\s*==\s*\(3,\s*{minor}\) @@ -53,8 +61,10 @@ rules: sys\.version_info\s*>=\s*\(3,\s*{minor}\) - | sys\.version_info\s*<=\s*\(3,\s*{minor}\) + # Matches sys.version_info > (3, 6) (equivalent to >=3.7) - | sys\.version_info\s*>\s*\(3,\s*{minor_minus_one}\) + # Matches sys.version_info < (3, 8) (equivalent to <=3.7) - | sys\.version_info\s*<\s*\(3,\s*{minor_plus_one}\) - | @@ -63,10 +73,24 @@ rules: sys\.version_info\.minor\s*>=\s*{minor}(?!\d) - | sys\.version_info\.minor\s*<=\s*{minor}(?!\d) + # Matches sys.version_info.minor > 6 (equivalent to >=7) - | sys\.version_info\.minor\s*>\s*{minor_minus_one}(?!\d) + # Matches sys.version_info.minor < 8 (equivalent to <=7) - | sys\.version_info\.minor\s*<\s*{minor_plus_one}(?!\d) + - | + sys\.version_info\[1\]\s*==\s*{minor}(?!\d) + - | + sys\.version_info\[1\]\s*>=\s*{minor}(?!\d) + - | + sys\.version_info\[1\]\s*<=\s*{minor}(?!\d) + # Matches sys.version_info[1] > 6 (equivalent to >=7) + - | + sys\.version_info\[1\]\s*>\s*{minor_minus_one}(?!\d) + # Matches sys.version_info[1] < 8 (equivalent to <=7) + - | + sys\.version_info\[1\]\s*<\s*{minor_plus_one}(?!\d) - name: python_env_short description: Finds short python environment names often used in tox or nox. @@ -99,4 +123,16 @@ rules: - | Python{major}{minor}(?!\d) + - name: dependency_requirement + description: Finds standard dependency requirement formats (e.g., protobuf==3.7). + examples: + - "protobuf==3.7" + - "protobuf>=3.7" + - "protobuf<=3.7" + - "protobuf~=3.7" + - "protobuf!=3.7" + rules: + - | + {name}\s*(?:==|>=|<=|~=|!=)\s*{version} + diff --git a/scripts/version_scanner/tests/unit/test_version_scanner.py b/scripts/version_scanner/tests/unit/test_version_scanner.py index f887dfc12fd4..69da8b822d32 100644 --- a/scripts/version_scanner/tests/unit/test_version_scanner.py +++ b/scripts/version_scanner/tests/unit/test_version_scanner.py @@ -410,8 +410,8 @@ def test_regex_examples_from_config(): rules_list = config.get("rules", []) - # Variables for interpolation (simulate Python 3.7) - vars = { + # Base variables for interpolation (simulate target version 3.7) + base_vars = { "major": "3", "minor": "7", "version": "3.7", @@ -427,6 +427,11 @@ def test_regex_examples_from_config(): if not examples or not templates: continue + # Resolve target dependency name based on applies_to metadata, falling back to protobuf + applies_to = rule_group.get("applies_to", []) + dep_name = applies_to[0] if applies_to else "protobuf" + vars = {**base_vars, "name": dep_name} + compiled_patterns = [] for template in templates: try: From 0c03af12aacadba6392b4bf8f3a01e58dbe88745 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Tue, 16 Jun 2026 11:21:11 -0400 Subject: [PATCH 2/2] fix(version-scanner): tighten version boundaries, add lookaheads, allow subscript whitespace --- scripts/version_scanner/regex_config.yaml | 20 ++++---- .../tests/unit/test_version_scanner.py | 51 +++++++++++++++++++ 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/scripts/version_scanner/regex_config.yaml b/scripts/version_scanner/regex_config.yaml index 312e7c67d7f0..c88696016bfc 100644 --- a/scripts/version_scanner/regex_config.yaml +++ b/scripts/version_scanner/regex_config.yaml @@ -25,15 +25,15 @@ rules: - | python_requires\s*=\s*['"]==3\.{minor}(?:\.\d+)?['"] - | - python_requires\s*=\s*['"]>=3\.{minor}(?:\.\d+)?['"] + python_requires\s*=\s*['"]>=3\.{minor}(?:\.0)?['"] - | - python_requires\s*=\s*['"]<=3\.{minor}(?:\.\d+)?['"] + python_requires\s*=\s*['"]<=3\.{minor}(?:\.0)?['"] # Matches >3.6 (equivalent to >=3.7) - | - python_requires\s*=\s*['"]>3\.{minor_minus_one}(?:\.\d+)?['"] + python_requires\s*=\s*['"]>3\.{minor_minus_one}(?:\.0)?['"] # Matches <3.8 (equivalent to <=3.7) - | - python_requires\s*=\s*['"]<3\.{minor_plus_one}(?:\.\d+)?['"] + python_requires\s*=\s*['"]<3\.{minor_plus_one}(?:\.0)?['"] - name: sys_version_info description: Finds sys.version_info checks in code. @@ -80,17 +80,17 @@ rules: - | sys\.version_info\.minor\s*<\s*{minor_plus_one}(?!\d) - | - sys\.version_info\[1\]\s*==\s*{minor}(?!\d) + sys\.version_info\[\s*1\s*\]\s*==\s*{minor}(?!\d) - | - sys\.version_info\[1\]\s*>=\s*{minor}(?!\d) + sys\.version_info\[\s*1\s*\]\s*>=\s*{minor}(?!\d) - | - sys\.version_info\[1\]\s*<=\s*{minor}(?!\d) + sys\.version_info\[\s*1\s*\]\s*<=\s*{minor}(?!\d) # Matches sys.version_info[1] > 6 (equivalent to >=7) - | - sys\.version_info\[1\]\s*>\s*{minor_minus_one}(?!\d) + sys\.version_info\[\s*1\s*\]\s*>\s*{minor_minus_one}(?!\d) # Matches sys.version_info[1] < 8 (equivalent to <=7) - | - sys\.version_info\[1\]\s*<\s*{minor_plus_one}(?!\d) + sys\.version_info\[\s*1\s*\]\s*<\s*{minor_plus_one}(?!\d) - name: python_env_short description: Finds short python environment names often used in tox or nox. @@ -133,6 +133,6 @@ rules: - "protobuf!=3.7" rules: - | - {name}\s*(?:==|>=|<=|~=|!=)\s*{version} + {name}\s*(?:==|>=|<=|~=|!=)\s*{version}(?!\d) diff --git a/scripts/version_scanner/tests/unit/test_version_scanner.py b/scripts/version_scanner/tests/unit/test_version_scanner.py index 69da8b822d32..f76b66d4d55d 100644 --- a/scripts/version_scanner/tests/unit/test_version_scanner.py +++ b/scripts/version_scanner/tests/unit/test_version_scanner.py @@ -448,6 +448,57 @@ def test_regex_examples_from_config(): break assert matched, f"Example '{example}' in group '{name}' did not match any pattern." + +def test_regex_negative_cases(): + """Verify regex patterns prevent false positives (lookaheads, patch bounds) and support whitespace.""" + config_path = "regex_config.yaml" + with open(config_path, 'r') as f: + config = yaml.safe_load(f) + + rules_list = config.get("rules", []) + + # Target version 3.7 + vars = { + "name": "protobuf", + "major": "3", + "minor": "7", + "version": "3.7", + "minor_plus_one": "8", + "minor_minus_one": "6" + } + + # Find specific rule groups + dep_req_group = next(r for r in rules_list if r["name"] == "dependency_requirement") + python_cmd_group = next(r for r in rules_list if r["name"] == "explicit_python_command") + python_req_group = next(r for r in rules_list if r["name"] == "python_requires") + sys_info_group = next(r for r in rules_list if r["name"] == "sys_version_info") + + # 1. Verify dependency_requirement looks ahead correctly (no partial match) + dep_pattern = re.compile(dep_req_group["rules"][0].strip().format(**vars), re.IGNORECASE) + assert dep_pattern.search("protobuf==3.7") + assert not dep_pattern.search("protobuf==3.72") + + # 2. Verify explicit_python_command negative lookahead + cmd_pattern = re.compile(python_cmd_group["rules"][0].strip().format(**vars), re.IGNORECASE) + assert cmd_pattern.search("python3.7") + assert not cmd_pattern.search("python3.72") + + # 3. Verify python_requires optional patch limits boundary rules to .0 + # Boundary rule 1: >=3.7 (python_requires = '>=3.7.0' is OK, but >=3.7.1 is not equivalent and should be skipped) + req_ge_pattern = re.compile(python_req_group["rules"][1].strip().format(**vars), re.IGNORECASE) + assert req_ge_pattern.search("python_requires = '>=3.7'") + assert req_ge_pattern.search("python_requires = '>=3.7.0'") + assert not req_ge_pattern.search("python_requires = '>=3.7.1'") + + # 4. Verify sys_version_info[1] allows optional whitespace + # Matches sys.version_info[ 1 ] + sys_sub_pattern = re.compile(sys_info_group["rules"][10].strip().format(**vars), re.IGNORECASE) # sys.version_info[1] == 7 + assert sys_sub_pattern.search("sys.version_info[1] == 7") + assert sys_sub_pattern.search("sys.version_info[ 1 ] == 7") + assert sys_sub_pattern.search("sys.version_info[1 ] == 7") + assert sys_sub_pattern.search("sys.version_info[ 1] == 7") + + def test_main_exit_code_1(): """Test that main() calls sys.exit(1) when matches are found.""" # We can mock scan_repository to return a dummy match