Skip to content
Open
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
46 changes: 41 additions & 5 deletions scripts/version_scanner/regex_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}(?:\.0)?['"]
- |
python_requires\s*=\s*['"]<=3\.{minor}['"]
python_requires\s*=\s*['"]<=3\.{minor}(?:\.0)?['"]
# Matches >3.6 (equivalent to >=3.7)
- |
python_requires\s*=\s*['"]>3\.{minor_minus_one}['"]
python_requires\s*=\s*['"]>3\.{minor_minus_one}(?:\.0)?['"]
# Matches <3.8 (equivalent to <=3.7)
- |
python_requires\s*=\s*['"]<3\.{minor_plus_one}['"]
python_requires\s*=\s*['"]<3\.{minor_plus_one}(?:\.0)?['"]

- name: sys_version_info
description: Finds sys.version_info checks in code.
Expand All @@ -46,15 +49,22 @@ 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}\)
- |
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}\)
- |
Expand All @@ -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\[\s*1\s*\]\s*==\s*{minor}(?!\d)
- |
sys\.version_info\[\s*1\s*\]\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\[\s*1\s*\]\s*>\s*{minor_minus_one}(?!\d)
# Matches sys.version_info[1] < 8 (equivalent to <=7)
- |
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.
Expand Down Expand Up @@ -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}(?!\d)


60 changes: 58 additions & 2 deletions scripts/version_scanner/tests/unit/test_version_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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:
Expand All @@ -443,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
Expand Down
Loading