• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pantsbuild / pants / 20332790708

18 Dec 2025 09:48AM UTC coverage: 64.992% (-15.3%) from 80.295%
20332790708

Pull #22949

github

web-flow
Merge f730a56cd into 407284c67
Pull Request #22949: Add experimental uv resolver for Python lockfiles

54 of 97 new or added lines in 5 files covered. (55.67%)

8270 existing lines in 295 files now uncovered.

48990 of 75379 relevant lines covered (64.99%)

1.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

40.65
/src/python/pants/backend/tools/semgrep/rules_integration_test.py
1
# Copyright 2023 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
1✔
5

6
from collections.abc import Sequence
1✔
7
from textwrap import dedent
1✔
8

9
import pytest
1✔
10

11
from pants.core.goals.lint import LintResult, Partitions
1✔
12
from pants.core.target_types import FileTarget
1✔
13
from pants.core.util_rules import source_files
1✔
14
from pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest
1✔
15
from pants.engine.addresses import Address
1✔
16
from pants.engine.internals.native_engine import EMPTY_DIGEST
1✔
17
from pants.engine.target import Target
1✔
18
from pants.testutil.python_interpreter_selection import all_major_minor_python_versions
1✔
19
from pants.testutil.rule_runner import QueryRule, RuleRunner
1✔
20

21
from .rules import PartitionMetadata, SemgrepLintRequest
1✔
22
from .rules import rules as semgrep_rules
1✔
23
from .subsystem import SemgrepFieldSet, SemgrepSubsystem
1✔
24
from .subsystem import rules as semgrep_subsystem_rules
1✔
25

26
DIR = "src"
1✔
27
FILE = "file.txt"
1✔
28

29
# https://semgrep.dev/docs/cli-reference/#exit-codes
30
SEMGREP_ERROR_FAILURE_RETURN_CODE = 1
1✔
31

32
GOOD_FILE = "nothing_bad"
1✔
33
BAD_FILE = "bad_pattern\nalso_bad"
1✔
34
RULES = dedent(
1✔
35
    """\
36
    rules:
37
    - id: find-bad-pattern
38
      patterns:
39
        - pattern: bad_pattern
40
      message: >-
41
        bad pattern found!
42
      languages: [generic]
43
      severity: ERROR
44
      paths:
45
        # 'generic' means this finds itself
46
        exclude:
47
         - '*.yml'
48
    """
49
)
50
RULES2 = dedent(
1✔
51
    """\
52
    rules:
53
    - id: find-another-bad-pattern
54
      patterns:
55
        - pattern: also_bad
56
      message: >-
57
        second bad found!
58
      languages: [generic]
59
      severity: ERROR
60
      paths:
61
        # 'generic' means this finds itself
62
        exclude:
63
         - '*.yml'
64
    """
65
)
66

67
SINGLE_FILE_BUILD = dedent(
1✔
68
    f"""\
69
    file(name="f", source="{FILE}")
70
    """
71
)
72

73
BAD_FILE_LAYOUT = {
1✔
74
    f"{DIR}/{FILE}": BAD_FILE,
75
    f"{DIR}/.semgrep.yml": RULES,
76
    f"{DIR}/BUILD": SINGLE_FILE_BUILD,
77
}
78
GOOD_FILE_LAYOUT = {
1✔
79
    f"{DIR}/{FILE}": GOOD_FILE,
80
    f"{DIR}/.semgrep.yml": RULES,
81
    f"{DIR}/BUILD": SINGLE_FILE_BUILD,
82
}
83

84

85
@pytest.fixture
1✔
86
def rule_runner() -> RuleRunner:
1✔
87
    return RuleRunner(
1✔
88
        rules=[
89
            *semgrep_rules(),
90
            *semgrep_subsystem_rules(),
91
            *source_files.rules(),
92
            QueryRule(Partitions, (SemgrepLintRequest.PartitionRequest,)),
93
            QueryRule(LintResult, (SemgrepLintRequest.Batch,)),
94
            QueryRule(SourceFiles, (SourceFilesRequest,)),
95
        ],
96
        target_types=[FileTarget],
97
    )
98

99

100
def run_semgrep(
1✔
101
    rule_runner: RuleRunner,
102
    targets: list[Target],
103
    *,
104
    extra_args: Sequence[str] = (),
105
    use_default_semgrep_args: bool = False,
106
) -> tuple[LintResult, ...]:
107
    if not use_default_semgrep_args:
1✔
108
        # clear out the default --quiet so that we can check the 'Ran ... rules on ... files:
109
        # ... findings' output
110
        extra_args = ("--semgrep-args=[]", *extra_args)
1✔
111

112
    rule_runner.set_options(
1✔
113
        [
114
            "--backend-packages=pants.backend.tools.semgrep",
115
            f"--python-interpreter-constraints={SemgrepSubsystem.default_interpreter_constraints!r}",
116
            *extra_args,
117
        ],
118
        env_inherit={"PATH", "PYENV_ROOT", "HOME"},
119
    )
120
    partitions = rule_runner.request(
1✔
121
        Partitions[SemgrepFieldSet, PartitionMetadata],
122
        [
123
            SemgrepLintRequest.PartitionRequest(
124
                tuple(SemgrepFieldSet.create(tgt) for tgt in targets)
125
            )
126
        ],
127
    )
128

129
    return tuple(
1✔
130
        rule_runner.request(
131
            LintResult,
132
            [SemgrepLintRequest.Batch("", partition.elements, partition.metadata)],
133
        )
134
        for partition in partitions
135
    )
136

137

138
def assert_success(
1✔
139
    rule_runner: RuleRunner, target: Target, *, extra_args: Sequence[str] = ()
140
) -> None:
UNCOV
141
    results = run_semgrep(rule_runner, [target], extra_args=extra_args)
×
142

UNCOV
143
    assert len(results) == 1
×
UNCOV
144
    result = results[0]
×
UNCOV
145
    assert result.stdout == ""
×
UNCOV
146
    assert "Ran 1 rule on 1 file: 0 findings" in result.stderr
×
UNCOV
147
    assert result.exit_code == 0
×
UNCOV
148
    assert result.report == EMPTY_DIGEST
×
149

150

151
@pytest.mark.parametrize(
1✔
152
    "major_minor_interpreter",
153
    all_major_minor_python_versions(SemgrepSubsystem.default_interpreter_constraints),
154
)
155
def test_passing(rule_runner: RuleRunner, major_minor_interpreter: str) -> None:
1✔
UNCOV
156
    rule_runner.write_files(GOOD_FILE_LAYOUT)
×
UNCOV
157
    tgt = rule_runner.get_target(Address(DIR, target_name="f"))
×
UNCOV
158
    assert_success(
×
159
        rule_runner,
160
        tgt,
161
        extra_args=[f"--python-interpreter-constraints=['=={major_minor_interpreter}.*']"],
162
    )
163

164

165
@pytest.mark.platform_specific_behavior
1✔
166
@pytest.mark.parametrize(
1✔
167
    "files,config_name",
168
    [
169
        pytest.param(
170
            BAD_FILE_LAYOUT,
171
            None,
172
        ),
173
        pytest.param(
174
            {
175
                f"{DIR}/bad.txt": BAD_FILE,
176
                f"{DIR}/BUILD": """file(name="f", source="bad.txt")""",
177
                ".custom_semgrep_file.yml": RULES,
178
            },
179
            ".custom_semgrep_file.yml",
180
            id="via custom config file",
181
        ),
182
    ],
183
)
184
def test_failing(rule_runner: RuleRunner, files: dict[str, str], config_name: str | None) -> None:
1✔
185
    rule_runner.write_files(files)
1✔
186
    tgt = rule_runner.get_target(Address(DIR, target_name="f"))
1✔
187

188
    extra_args = [f"--semgrep-config-name={config_name}"] if config_name else []
1✔
189
    results = run_semgrep(rule_runner, [tgt], extra_args=extra_args)
1✔
190
    assert len(results) == 1
1✔
191
    result = results[0]
1✔
192
    assert "find-bad-pattern" in result.stdout
1✔
193
    assert "Ran 1 rule on 1 file: 1 finding" in result.stderr
1✔
194
    assert result.exit_code == SEMGREP_ERROR_FAILURE_RETURN_CODE
1✔
195
    assert result.report == EMPTY_DIGEST
1✔
196

197

198
def test_multiple_targets(rule_runner: RuleRunner) -> None:
1✔
UNCOV
199
    rule_runner.write_files(
×
200
        {
201
            f"{DIR}/good.txt": GOOD_FILE,
202
            f"{DIR}/bad.txt": BAD_FILE,
203
            f"{DIR}/.semgrep.yml": RULES,
204
            f"{DIR}/BUILD": dedent(
205
                """\
206
                file(name="g", source="good.txt")
207
                file(name="b", source="bad.txt")
208
                """
209
            ),
210
        }
211
    )
212

UNCOV
213
    tgts = [rule_runner.get_target(Address(DIR, target_name=name)) for name in ["g", "b"]]
×
214

UNCOV
215
    results = run_semgrep(
×
216
        rule_runner,
217
        tgts,
218
    )
UNCOV
219
    assert len(results) == 1
×
UNCOV
220
    result = results[0]
×
UNCOV
221
    assert "find-bad-pattern" in result.stdout
×
UNCOV
222
    assert "Ran 1 rule on 2 files: 1 finding" in result.stderr
×
UNCOV
223
    assert result.exit_code == SEMGREP_ERROR_FAILURE_RETURN_CODE
×
UNCOV
224
    assert result.report == EMPTY_DIGEST
×
225

226

227
@pytest.mark.parametrize(
1✔
228
    "files,config_name",
229
    [
230
        pytest.param(
231
            {
232
                **BAD_FILE_LAYOUT,
233
                ".semgrep.yml": RULES2,
234
            },
235
            None,
236
            id="via nesting",
237
        ),
238
        pytest.param(
239
            {
240
                f"{DIR}/bad.txt": BAD_FILE,
241
                f"{DIR}/BUILD": """file(name="f", source="bad.txt")""",
242
                ".semgrep/one.yml": RULES,
243
                ".semgrep/vendored/two.yml": RULES2,
244
            },
245
            None,
246
            id="via .semgrep directory",
247
        ),
248
        pytest.param(
249
            {
250
                f"{DIR}/bad.txt": BAD_FILE,
251
                f"{DIR}/BUILD": """file(name="f", source="bad.txt")""",
252
                ".semgrep/one.yml": RULES,
253
                ".semgrep/vendored/two.yml": RULES2,
254
            },
255
            None,
256
            id="via recursive .semgrep directory",
257
        ),
258
        pytest.param(
259
            {
260
                f"{DIR}/bad.txt": BAD_FILE,
261
                f"{DIR}/BUILD": """file(name="f", source="bad.txt")""",
262
                "custom_semgrep_dir/one.yml": RULES,
263
                "custom_semgrep_dir/vendored/two.yml": RULES2,
264
            },
265
            "custom_semgrep_dir",
266
            id="via custom recursive config directory",
267
        ),
268
    ],
269
)
270
def test_multiple_configs(
1✔
271
    rule_runner: RuleRunner, files: dict[str, str], config_name: str | None
272
) -> None:
UNCOV
273
    rule_runner.write_files(files)
×
274

UNCOV
275
    tgt = rule_runner.get_target(Address(DIR, target_name="f"))
×
UNCOV
276
    extra_args = [f"--semgrep-config-name={config_name}"] if config_name else []
×
UNCOV
277
    results = run_semgrep(rule_runner, [tgt], extra_args=extra_args)
×
278

UNCOV
279
    assert len(results) == 1
×
UNCOV
280
    result = results[0]
×
UNCOV
281
    assert "find-bad-pattern" in result.stdout
×
UNCOV
282
    assert "find-another-bad-pattern" in result.stdout
×
UNCOV
283
    assert "Ran 2 rules on 1 file: 2 findings" in result.stderr
×
UNCOV
284
    assert result.exit_code == SEMGREP_ERROR_FAILURE_RETURN_CODE
×
UNCOV
285
    assert result.report == EMPTY_DIGEST
×
286

287

288
def test_semgrepignore(rule_runner: RuleRunner) -> None:
1✔
UNCOV
289
    rule_runner.write_files({**BAD_FILE_LAYOUT, ".semgrepignore": FILE})
×
290

UNCOV
291
    tgt = rule_runner.get_target(Address(DIR, target_name="f"))
×
UNCOV
292
    results = run_semgrep(rule_runner, [tgt])
×
293

UNCOV
294
    assert len(results) == 1
×
UNCOV
295
    result = results[0]
×
UNCOV
296
    assert result.stdout == ""
×
UNCOV
297
    assert "Ran 1 rule on 0 files: 0 findings" in result.stderr
×
UNCOV
298
    assert result.exit_code == 0
×
UNCOV
299
    assert result.report == EMPTY_DIGEST
×
300

301

302
def test_partition_by_config(rule_runner: RuleRunner) -> None:
1✔
UNCOV
303
    file_dirs = []
×
304

UNCOV
305
    def file___(dir: str) -> dict[str, str]:
×
UNCOV
306
        file_dirs.append(dir)
×
UNCOV
307
        return {
×
308
            f"{dir}/{FILE}": GOOD_FILE,
309
            f"{dir}/BUILD": f"""file(name="f", source="{FILE}")""",
310
        }
311

UNCOV
312
    def semgrep(dir: str) -> dict[str, str]:
×
UNCOV
313
        return {f"{dir}/.semgrep.yml": RULES}
×
314

UNCOV
315
    rule_runner.write_files(
×
316
        {
317
            # 'y'/'n' indicates whether that level has semgrep config
318
            **semgrep("y"),
319
            **file___("y/dir1"),
320
            **file___("y/dir2"),
321
            **file___("y/n/dir1"),
322
            **file___("y/n/dir2"),
323
            **semgrep("y/y"),
324
            **file___("y/y/dir1"),
325
            **file___("y/y/dir2"),
326
            **file___("n"),
327
        }
328
    )
329

UNCOV
330
    field_sets = tuple(
×
331
        SemgrepFieldSet.create(rule_runner.get_target(Address(dir, target_name="f")))
332
        for dir in file_dirs
333
    )
334

UNCOV
335
    partitions = rule_runner.request(
×
336
        Partitions[SemgrepFieldSet, PartitionMetadata],
337
        [SemgrepLintRequest.PartitionRequest(field_sets)],
338
    )
339

UNCOV
340
    sorted_partitions = sorted(
×
341
        (
342
            sorted(field_set.address.spec for field_set in partition.elements),
343
            sorted(str(f) for f in partition.metadata.config_files),
344
        )
345
        for partition in partitions
346
    )
347

UNCOV
348
    assert sorted_partitions == [
×
349
        (
350
            ["y/dir1:f", "y/dir2:f", "y/n/dir1:f", "y/n/dir2:f"],
351
            ["y/.semgrep.yml"],
352
        ),
353
        (
354
            ["y/y/dir1:f", "y/y/dir2:f"],
355
            ["y/.semgrep.yml", "y/y/.semgrep.yml"],
356
        ),
357
        # n: doesn't appear in any partition
358
    ]
359

360

361
def test_skip(rule_runner: RuleRunner) -> None:
1✔
UNCOV
362
    rule_runner.write_files(BAD_FILE_LAYOUT)
×
UNCOV
363
    tgt = rule_runner.get_target(Address(DIR, target_name="f"))
×
364

UNCOV
365
    results = run_semgrep(rule_runner, [tgt], extra_args=["--semgrep-skip"])
×
UNCOV
366
    assert not results
×
367

368

369
@pytest.mark.xfail(
1✔
370
    reason=""" TODO: --semgrep-force does rerun the underlying process, but the LintResult's
371
    contents are the same (same stdout etc.), these are deduped, and thus we cannot detect the
372
    rerun""",
373
    # no point spending time on this
374
    run=False,
375
)
376
def test_force(rule_runner: RuleRunner) -> None:
1✔
377
    rule_runner.write_files(GOOD_FILE_LAYOUT)
×
378
    tgt = rule_runner.get_target(Address(DIR, target_name="f"))
×
379

380
    # Should not receive a memoized result if force=True.
381
    results1 = run_semgrep(rule_runner, [tgt], extra_args=["--semgrep-force"])
×
382
    results2 = run_semgrep(rule_runner, [tgt], extra_args=["--semgrep-force"])
×
383

384
    assert len(results1) == len(results2) == 1
×
385
    assert results1[0].exit_code == results2[0].exit_code == 0
×
386
    assert results1[0] is not results2[0]
×
387

388
    # But should if force=False.
389
    results1 = run_semgrep(rule_runner, [tgt])
×
390
    results2 = run_semgrep(rule_runner, [tgt])
×
391
    assert len(results1) == len(results2) == 1
×
392
    assert results1[0].exit_code == results2[0].exit_code == 0
×
393
    assert results1[0] is results2[0]
×
394

395

396
def test_default_args(rule_runner: RuleRunner) -> None:
1✔
UNCOV
397
    rule_runner.write_files(BAD_FILE_LAYOUT)
×
UNCOV
398
    tgt = rule_runner.get_target(Address(DIR, target_name="f"))
×
399

UNCOV
400
    results = run_semgrep(rule_runner, [tgt], use_default_semgrep_args=True)
×
UNCOV
401
    assert len(results) == 1
×
UNCOV
402
    result = results[0]
×
UNCOV
403
    assert "find-bad-pattern" in result.stdout
×
UNCOV
404
    assert result.stderr == ""
×
UNCOV
405
    assert result.exit_code == SEMGREP_ERROR_FAILURE_RETURN_CODE
×
UNCOV
406
    assert result.report == EMPTY_DIGEST
×
407

408

409
def test_extra_args(rule_runner: RuleRunner) -> None:
1✔
UNCOV
410
    rule_runner.write_files(BAD_FILE_LAYOUT)
×
UNCOV
411
    tgt = rule_runner.get_target(Address(DIR, target_name="f"))
×
412

UNCOV
413
    results = run_semgrep(rule_runner, [tgt], extra_args=["--semgrep-args=--quiet"])
×
UNCOV
414
    assert len(results) == 1
×
UNCOV
415
    result = results[0]
×
UNCOV
416
    assert "find-bad-pattern" in result.stdout
×
UNCOV
417
    assert result.stderr == ""
×
UNCOV
418
    assert result.exit_code == SEMGREP_ERROR_FAILURE_RETURN_CODE
×
UNCOV
419
    assert result.report == EMPTY_DIGEST
×
420

421

422
def test_semgrep_pex_contents_is_ignored(rule_runner: RuleRunner) -> None:
1✔
UNCOV
423
    rule_runner.write_files(
×
424
        {
425
            # Validate that this doesn't traverse into the PEX created for semgrep itself: a
426
            # top-level __main__.py will translate into --include=__main__.py (aka **/__main__.py)
427
            # which will, naively, find the __main__.py in the PEX.
428
            "__main__.py": "",
429
            ".semgrep.yml": RULES,
430
            "BUILD": """file(name="f", source="__main__.py")""",
431
        }
432
    )
433

UNCOV
434
    tgt = rule_runner.get_target(Address("", target_name="f"))
×
UNCOV
435
    results = run_semgrep(rule_runner, [tgt])
×
436

UNCOV
437
    assert len(results) == 1
×
UNCOV
438
    result = results[0]
×
UNCOV
439
    assert result.stdout == ""
×
440
    # Without the --exclude, this would run on 2 files.
UNCOV
441
    assert "Ran 1 rule on 1 file: 0 findings" in result.stderr
×
UNCOV
442
    assert result.exit_code == 0
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc