Skip to content

Commit ad02121

Browse files
authored
feat(version-scanner): implement targeted namespaced ignore pragmas (#17540)
This PR implements ignore pragmas to allow granular and self-expiring ignores to help fend off false positives in places where we need to leave references to specific strings: `# version-scanner: ignore-rule=rule_name:version` ### Example: Imagine searching for "Python 3.7". We have a variety of rules to cover multiple circumstances. One rule matches solely on the version number (e.g. "3.7" in case the reference is an edge case not covered by other more specific and complicated rules). Thus if we find "3.7" in a situation unrelated to Python, we will have a false positive such as this: `matplotlib==3.7.2` We can flag a line like this to be ignored under one of the numerous rules we have for categorizing matches (e.g `explicit_version_string`, `dependency_requirement`, `combined_version_string`, etc.) so that it does not trigger a false positive in the future. In this case we detected it under the `explicit_version_string` pattern so we flag it to be ignored under that rule, if the search is for 3.7. `matplotlib==3.7.2 # version-scanner: ignore-rule=explicit_version_string:3.7` You might ask: **but what about when we need to find versions of matplotlib so we can update references to them?** If instead of Python, we were to search for "matplotlib 3.7.2" we would get a match. As a non-Python match, it would be caught under the `dependency_requirement` rule instead of the `explicit_*` rule.
1 parent 8539a8c commit ad02121

2 files changed

Lines changed: 56 additions & 1 deletion

File tree

scripts/version_scanner/tests/unit/test_version_scanner.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,56 @@ def test_scan_file_ignores_pragma(tmp_path):
148148
results = scan_file(str(test_file), rules)
149149
assert len(results) == 0
150150

151+
152+
def test_scan_file_ignores_targeted_pragma(tmp_path):
153+
test_file = tmp_path / "test.py"
154+
test_file.write_text("python_requires = '>=3.7' # version-scanner: ignore-rule=python_requires_check:3.7\n")
155+
156+
rules = [
157+
{
158+
"name": "python_requires_check",
159+
"pattern": re.compile(r"python_requires\s*=\s*['\"]>=3\.7['\"]"),
160+
"version": "3.7"
161+
}
162+
]
163+
164+
results = scan_file(str(test_file), rules)
165+
assert len(results) == 0
166+
167+
168+
def test_scan_file_handles_non_string_rule_name_and_version(tmp_path):
169+
test_file = tmp_path / "test.py"
170+
test_file.write_text("python_requires = '>=3.7' # version-scanner: ignore-rule=123:3.7\n")
171+
172+
rules = [
173+
{
174+
"name": 123, # Integer rule name
175+
"pattern": re.compile(r"python_requires\s*=\s*['\"]>=3\.7['\"]"),
176+
"version": 3.7 # Float version
177+
}
178+
]
179+
180+
# This should not raise TypeError
181+
results = scan_file(str(test_file), rules)
182+
assert len(results) == 0
183+
184+
185+
186+
def test_scan_file_does_not_ignore_mismatched_targeted_pragma(tmp_path):
187+
test_file = tmp_path / "test.py"
188+
test_file.write_text("python_requires = '>=3.7' # version-scanner: ignore-rule=python_requires_check:3.8\n")
189+
190+
rules = [
191+
{
192+
"name": "python_requires_check",
193+
"pattern": re.compile(r"python_requires\s*=\s*['\"]>=3\.7['\"]"),
194+
"version": "3.7"
195+
}
196+
]
197+
198+
results = scan_file(str(test_file), rules)
199+
assert len(results) == 1
200+
151201
def test_scan_file_ignores_next_line(tmp_path):
152202
test_file = tmp_path / "test.py"
153203
test_file.write_text("# version-scanner: ignore-next-line\npython_requires = '>=3.7'\n")

scripts/version_scanner/version_scanner.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,11 +214,16 @@ def scan_file(file_path: str, compiled_rules: List[Dict[str, re.Pattern]]) -> Li
214214
if "version-scanner: ignore-next-line" in line:
215215
skip_next = True
216216
continue
217-
if "version-scanner: ignore" in line:
217+
if "version-scanner: ignore" in line and "version-scanner: ignore-rule" not in line and "version-scanner: ignore-next-line" not in line:
218218
continue
219219
for rule in compiled_rules:
220220
match = rule["pattern"].search(line)
221221
if match:
222+
version = rule.get("version")
223+
if version:
224+
pragma_pattern = rf"version-scanner\s*:\s*ignore-rule\s*=\s*{re.escape(str(rule['name']))}\s*:\s*{re.escape(str(version))}"
225+
if re.search(pragma_pattern, line, re.IGNORECASE):
226+
continue
222227
results.append({
223228
"rule_name": rule["name"],
224229
"line_number": line_num,

0 commit comments

Comments
 (0)