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

pantsbuild / pants / 25441711719

06 May 2026 02:31PM UTC coverage: 92.915%. Remained the same
25441711719

push

github

web-flow
use sha pin (with comment) format for generated actions (#23312)

Per the GitHub Action best practices we recently enabled at #23249, we
should pin each action to a SHA so that the reference is actually
immutable.

This will -- I hope -- knock out a large chunk of the 421 alerts we
currently get from zizmor. The next followup would then be upgrades and
harmonizing the generated and none-generated pins.

Notice: This idea was suggested by Claude while going over pinact output
and I was surprised to see that post processing the yaml wasn't too
gross.

92206 of 99237 relevant lines covered (92.91%)

4.04 hits per line

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

98.9
/src/python/pants/backend/scala/target_types.py
1
# Copyright 2020 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
9✔
5

6
from dataclasses import dataclass
9✔
7
from typing import ClassVar
9✔
8

9
from pants.backend.scala.subsystems.scala import ScalaSubsystem
9✔
10
from pants.backend.scala.subsystems.scala_infer import ScalaInferSubsystem
9✔
11
from pants.backend.scala.util_rules.versions import ScalaCrossVersionMode
9✔
12
from pants.build_graph.address import AddressInput
9✔
13
from pants.build_graph.build_file_aliases import BuildFileAliases
9✔
14
from pants.core.goals.test import TestExtraEnvVarsField, TestTimeoutField
9✔
15
from pants.engine.addresses import Address
9✔
16
from pants.engine.rules import collect_rules, rule
9✔
17
from pants.engine.target import (
9✔
18
    COMMON_TARGET_FIELDS,
19
    AsyncFieldMixin,
20
    Dependencies,
21
    FieldSet,
22
    GeneratedTargets,
23
    GenerateTargetsRequest,
24
    MultipleSourcesField,
25
    OverridesField,
26
    SingleSourceField,
27
    StringField,
28
    StringSequenceField,
29
    Target,
30
    TargetFilesGenerator,
31
    TargetFilesGeneratorSettings,
32
    TargetFilesGeneratorSettingsRequest,
33
    TargetGenerator,
34
    generate_file_based_overrides_field_help_message,
35
    generate_multiple_sources_field_help_message,
36
)
37
from pants.engine.unions import UnionMembership, UnionRule
9✔
38
from pants.jvm import target_types as jvm_target_types
9✔
39
from pants.jvm.run import jvm_run_rules
9✔
40
from pants.jvm.subsystems import JvmSubsystem
9✔
41
from pants.jvm.target_types import (
9✔
42
    JunitTestExtraEnvVarsField,
43
    JunitTestSourceField,
44
    JunitTestTimeoutField,
45
    JvmArtifactArtifactField,
46
    JvmArtifactExclusion,
47
    JvmArtifactExclusionsField,
48
    JvmArtifactGroupField,
49
    JvmArtifactJarSourceField,
50
    JvmArtifactPackagesField,
51
    JvmArtifactResolveField,
52
    JvmArtifactTarget,
53
    JvmArtifactUrlField,
54
    JvmArtifactVersionField,
55
    JvmJdkField,
56
    JvmMainClassNameField,
57
    JvmProvidesTypesField,
58
    JvmResolveField,
59
    JvmRunnableSourceFieldSet,
60
    _jvm_artifact_exclusions_field_help,
61
)
62
from pants.util.strutil import help_text, softwrap
9✔
63

64

65
class ScalaSettingsRequest(TargetFilesGeneratorSettingsRequest):
9✔
66
    pass
9✔
67

68

69
@rule
9✔
70
async def scala_settings_request(
9✔
71
    scala_infer_subsystem: ScalaInferSubsystem, _: ScalaSettingsRequest
72
) -> TargetFilesGeneratorSettings:
73
    return TargetFilesGeneratorSettings(
9✔
74
        add_dependencies_on_all_siblings=scala_infer_subsystem.force_add_siblings_as_dependencies
75
        or not scala_infer_subsystem.imports
76
    )
77

78

79
class ScalaSourceField(SingleSourceField):
9✔
80
    expected_file_extensions = (".scala",)
9✔
81

82

83
class ScalaGeneratorSourcesField(MultipleSourcesField):
9✔
84
    expected_file_extensions = (".scala",)
9✔
85

86

87
class ScalaDependenciesField(Dependencies):
9✔
88
    pass
9✔
89

90

91
class ScalaConsumedPluginNamesField(StringSequenceField):
9✔
92
    help = help_text(
9✔
93
        """
94
        The names of Scala plugins that this source file requires.
95

96
        The plugin must be defined by a corresponding `scalac_plugin` AND `jvm_artifact` target,
97
        and must be present in this target's resolve's lockfile.
98

99
        If not specified, this will default to the plugins specified in
100
        `[scalac].plugins_for_resolve` for this target's resolve.
101
        """
102
    )
103

104
    alias = "scalac_plugins"
9✔
105
    required = False
9✔
106

107

108
@dataclass(frozen=True)
9✔
109
class ScalaFieldSet(JvmRunnableSourceFieldSet):
9✔
110
    required_fields = (ScalaSourceField,)
9✔
111

112
    sources: ScalaSourceField
9✔
113

114

115
@dataclass(frozen=True)
9✔
116
class ScalaGeneratorFieldSet(FieldSet):
9✔
117
    required_fields = (ScalaGeneratorSourcesField,)
9✔
118

119
    sources: ScalaGeneratorSourcesField
9✔
120

121

122
# -----------------------------------------------------------------------------------------------
123
# `scalatest_tests`
124
# -----------------------------------------------------------------------------------------------
125

126

127
class ScalatestTestSourceField(ScalaSourceField):
9✔
128
    pass
9✔
129

130

131
class ScalatestTestTimeoutField(TestTimeoutField):
9✔
132
    pass
9✔
133

134

135
class ScalatestTestExtraEnvVarsField(TestExtraEnvVarsField):
9✔
136
    pass
9✔
137

138

139
class ScalatestTestTarget(Target):
9✔
140
    alias = "scalatest_test"
9✔
141
    core_fields = (
9✔
142
        *COMMON_TARGET_FIELDS,
143
        ScalaDependenciesField,
144
        ScalatestTestSourceField,
145
        ScalaConsumedPluginNamesField,
146
        ScalatestTestTimeoutField,
147
        ScalatestTestExtraEnvVarsField,
148
        JvmResolveField,
149
        JvmProvidesTypesField,
150
        JvmJdkField,
151
    )
152
    help = "A single Scala test, run with Scalatest."
9✔
153

154

155
class ScalatestTestsGeneratorSourcesField(ScalaGeneratorSourcesField):
9✔
156
    default = ("*Spec.scala", "*Suite.scala")
9✔
157
    help = generate_multiple_sources_field_help_message(
9✔
158
        "Example: `sources=['*Spec.scala', '!SuiteIgnore.scala']`"
159
    )
160

161

162
class ScalatestTestsSourcesOverridesField(OverridesField):
9✔
163
    help = generate_file_based_overrides_field_help_message(
9✔
164
        "scalatest_tests",
165
        """
166
        overrides={
167
            "Foo.scala": {"dependencies": [":files"]},
168
            "Bar.scala": {"skip_scalafmt": True},
169
            ("Foo.scala", "Bar.scala"): {"tags": ["linter_disabled"]},
170
        }"
171
        """,
172
    )
173

174

175
class ScalatestTestsGeneratorTarget(TargetFilesGenerator):
9✔
176
    alias = "scalatest_tests"
9✔
177
    core_fields = (
9✔
178
        *COMMON_TARGET_FIELDS,
179
        ScalatestTestsGeneratorSourcesField,
180
        ScalatestTestsSourcesOverridesField,
181
    )
182
    generated_target_cls = ScalatestTestTarget
9✔
183
    copied_fields = COMMON_TARGET_FIELDS
9✔
184
    moved_fields = (
9✔
185
        ScalaDependenciesField,
186
        ScalaConsumedPluginNamesField,
187
        ScalatestTestTimeoutField,
188
        ScalatestTestExtraEnvVarsField,
189
        JvmJdkField,
190
        JvmProvidesTypesField,
191
        JvmResolveField,
192
    )
193
    settings_request_cls = ScalaSettingsRequest
9✔
194
    help = help_text(
9✔
195
        f"""
196
        Generate a `scalatest_test` target for each file in the `sources` field (defaults to
197
        all files in the directory matching `{ScalatestTestsGeneratorSourcesField.default}`).
198
        """
199
    )
200

201

202
# -----------------------------------------------------------------------------------------------
203
# `scala_junit_tests`
204
# -----------------------------------------------------------------------------------------------
205

206

207
class ScalaJunitTestSourceField(ScalaSourceField, JunitTestSourceField):
9✔
208
    pass
9✔
209

210

211
class ScalaJunitTestTarget(Target):
9✔
212
    alias = "scala_junit_test"
9✔
213
    core_fields = (
9✔
214
        *COMMON_TARGET_FIELDS,
215
        ScalaDependenciesField,
216
        ScalaJunitTestSourceField,
217
        ScalaConsumedPluginNamesField,
218
        JunitTestTimeoutField,
219
        JunitTestExtraEnvVarsField,
220
        JvmResolveField,
221
        JvmProvidesTypesField,
222
        JvmJdkField,
223
    )
224
    help = "A single Scala test, run with JUnit."
9✔
225

226

227
class ScalaJunitTestsGeneratorSourcesField(ScalaGeneratorSourcesField):
9✔
228
    default = ("*Test.scala",)
9✔
229
    help = generate_multiple_sources_field_help_message(
9✔
230
        "Example: `sources=['*Test.scala', '!TestIgnore.scala']`"
231
    )
232

233

234
class ScalaJunitTestsSourcesOverridesField(OverridesField):
9✔
235
    help = generate_file_based_overrides_field_help_message(
9✔
236
        "scala_junit_tests",
237
        """
238
        overrides={
239
            "Foo.scala": {"dependencies": [":files"]},
240
            "Bar.scala": {"skip_scalafmt": True},
241
            ("Foo.scala", "Bar.scala"): {"tags": ["linter_disabled"]},
242
        }"
243
        """,
244
    )
245

246

247
class ScalaJunitTestsGeneratorTarget(TargetFilesGenerator):
9✔
248
    alias = "scala_junit_tests"
9✔
249
    core_fields = (
9✔
250
        *COMMON_TARGET_FIELDS,
251
        ScalaJunitTestsGeneratorSourcesField,
252
        ScalaJunitTestsSourcesOverridesField,
253
        JunitTestTimeoutField,
254
    )
255
    generated_target_cls = ScalaJunitTestTarget
9✔
256
    copied_fields = COMMON_TARGET_FIELDS
9✔
257
    moved_fields = (
9✔
258
        ScalaDependenciesField,
259
        ScalaConsumedPluginNamesField,
260
        JunitTestTimeoutField,
261
        JunitTestExtraEnvVarsField,
262
        JvmJdkField,
263
        JvmProvidesTypesField,
264
        JvmResolveField,
265
    )
266
    settings_request_cls = ScalaSettingsRequest
9✔
267
    help = "Generate a `scala_junit_test` target for each file in the `sources` field."
9✔
268

269

270
# -----------------------------------------------------------------------------------------------
271
# `scala_source` target
272
# -----------------------------------------------------------------------------------------------
273

274

275
class ScalaSourceTarget(Target):
9✔
276
    alias = "scala_source"
9✔
277
    core_fields = (
9✔
278
        *COMMON_TARGET_FIELDS,
279
        ScalaDependenciesField,
280
        ScalaSourceField,
281
        ScalaConsumedPluginNamesField,
282
        JvmResolveField,
283
        JvmProvidesTypesField,
284
        JvmJdkField,
285
        JvmMainClassNameField,
286
    )
287
    help = "A single Scala source file containing application or library code."
9✔
288

289

290
# -----------------------------------------------------------------------------------------------
291
# `scala_sources` target generator
292
# -----------------------------------------------------------------------------------------------
293

294

295
class ScalaSourcesGeneratorSourcesField(ScalaGeneratorSourcesField):
9✔
296
    default = (
9✔
297
        "*.scala",
298
        *(f"!{pat}" for pat in (ScalaJunitTestsGeneratorSourcesField.default)),
299
        *(f"!{pat}" for pat in (ScalatestTestsGeneratorSourcesField.default)),
300
    )
301
    help = generate_multiple_sources_field_help_message(
9✔
302
        "Example: `sources=['Example.scala', 'New*.scala', '!OldIgnore.scala']`"
303
    )
304

305

306
class ScalaSourcesOverridesField(OverridesField):
9✔
307
    help = generate_file_based_overrides_field_help_message(
9✔
308
        "scala_sources",
309
        """
310
        overrides={
311
            "Foo.scala": {"dependencies": [":files"]},
312
            "Bar.scala": {"skip_scalafmt": True},
313
            ("Foo.scala", "Bar.scala"): {"tags": ["linter_disabled"]},
314
        }"
315
        """,
316
    )
317

318

319
class ScalaSourcesGeneratorTarget(TargetFilesGenerator):
9✔
320
    alias = "scala_sources"
9✔
321
    core_fields = (
9✔
322
        *COMMON_TARGET_FIELDS,
323
        ScalaSourcesGeneratorSourcesField,
324
        ScalaSourcesOverridesField,
325
    )
326
    generated_target_cls = ScalaSourceTarget
9✔
327
    copied_fields = COMMON_TARGET_FIELDS
9✔
328
    moved_fields = (
9✔
329
        ScalaDependenciesField,
330
        ScalaConsumedPluginNamesField,
331
        JvmResolveField,
332
        JvmJdkField,
333
        JvmMainClassNameField,
334
        JvmProvidesTypesField,
335
    )
336
    settings_request_cls = ScalaSettingsRequest
9✔
337
    help = "Generate a `scala_source` target for each file in the `sources` field."
9✔
338

339

340
# -----------------------------------------------------------------------------------------------
341
# `scalac_plugin` target
342
# -----------------------------------------------------------------------------------------------
343

344

345
class ScalacPluginArtifactField(StringField, AsyncFieldMixin):
9✔
346
    alias = "artifact"
9✔
347
    required = True
9✔
348
    value: str
9✔
349
    help = "The address of either a `jvm_artifact` or a `scala_artifact` that defines a plugin for `scalac`."
9✔
350

351
    def to_address_input(self) -> AddressInput:
9✔
352
        return AddressInput.parse(
2✔
353
            self.value,
354
            relative_to=self.address.spec_path,
355
            description_of_origin=(
356
                f"the `{self.alias}` field in the `{ScalacPluginTarget.alias}` target {self.address}"
357
            ),
358
        )
359

360

361
class ScalacPluginNameField(StringField):
9✔
362
    alias = "plugin_name"
9✔
363
    help = help_text(
9✔
364
        """
365
        The name that `scalac` should use to load the plugin.
366

367
        If not set, the plugin name defaults to the target name.
368
        """
369
    )
370

371

372
class ScalacPluginTarget(Target):
9✔
373
    alias = "scalac_plugin"
9✔
374
    core_fields = (
9✔
375
        *COMMON_TARGET_FIELDS,
376
        ScalacPluginArtifactField,
377
        ScalacPluginNameField,
378
    )
379
    help = help_text(
9✔
380
        """
381
        A plugin for `scalac`.
382

383
        Currently only thirdparty plugins are supported. To enable a plugin, define this
384
        target type, and set the `artifact=` field to the address of a `jvm_artifact` that
385
        provides the plugin.
386

387
        If the `scalac`-loaded name of the plugin does not match the target's name,
388
        additionally set the `plugin_name=` field.
389
        """
390
    )
391

392

393
# -----------------------------------------------------------------------------------------------
394
# `scala_artifact` target
395
# -----------------------------------------------------------------------------------------------
396

397

398
# Defining this field and making it required in the `ScalaArtifactFieldSet`
399
# prevents the `JvmArtifactFieldSet` matching against `scala_artifact` targets
400
# and raising an error when resolving a classpath in which such targets have been
401
# used as explicit dependencies of other targets.
402
#
403
# This way classpath entries for `scala_artifact` targets will be resolved using
404
# their own rules, bringing the actual JAR dependency as a transitive one.
405
class ScalaArtifactArtifactField(StringField):
9✔
406
    alias = "artifact"
9✔
407
    required = True
9✔
408
    value: str
9✔
409
    help = help_text(
9✔
410
        """
411
        The 'artifact' part of a Maven-compatible Scala-versioned coordinate to a third-party JAR artifact.
412

413
        For the JAR coordinate `org.typelevel:cats-core_2.13:2.9.0`, the artifact is `cats-core`.
414
        """
415
    )
416

417

418
class ScalaArtifactCrossversionField(StringField):
9✔
419
    alias = "crossversion"
9✔
420
    default = ScalaCrossVersionMode.BINARY.value
9✔
421
    help = help_text(
9✔
422
        f"""
423
        Whether to use the full Scala version or the partial one to determine the artifact name suffix.
424

425
        Default is `{ScalaCrossVersionMode.BINARY.value}`.
426
        """
427
    )
428
    valid_choices = ScalaCrossVersionMode
9✔
429

430

431
@dataclass(frozen=True)
9✔
432
class ScalaArtifactExclusion(JvmArtifactExclusion):
9✔
433
    alias = "scala_exclude"
9✔
434
    help = help_text(
9✔
435
        """
436
        Exclude the given `artifact` and `group`, or all artifacts from the given `group`.
437
        You can also use the `crossversion` field to help resolve the final artifact name.
438
        """
439
    )
440

441
    crossversion: str = ScalaCrossVersionMode.BINARY.value
9✔
442

443
    def validate(self, address: Address) -> set[str]:
9✔
444
        errors = super().validate(address)
1✔
445
        valid_crossversions = [x.value for x in ScalaCrossVersionMode]
1✔
446
        if self.crossversion not in valid_crossversions:
1✔
447
            errors.add(
×
448
                softwrap(
449
                    f"""
450
                    Invalid `crossversion` value '{self.crossversion}' in in list of
451
                    exclusions at target: {address}. Valid values are:
452
                    {", ".join(valid_crossversions)}
453
                    """
454
                )
455
            )
456
        return errors
1✔
457

458

459
class ScalaArtifactExclusionsField(JvmArtifactExclusionsField):
9✔
460
    help = _jvm_artifact_exclusions_field_help(
9✔
461
        lambda: ScalaArtifactExclusionsField.supported_exclusion_types
462
    )
463
    supported_exclusion_types: ClassVar[tuple[type[JvmArtifactExclusion], ...]] = (
9✔
464
        JvmArtifactExclusion,
465
        ScalaArtifactExclusion,
466
    )
467

468

469
@dataclass(frozen=True)
9✔
470
class ScalaArtifactFieldSet(FieldSet):
9✔
471
    group: JvmArtifactGroupField
9✔
472
    artifact: ScalaArtifactArtifactField
9✔
473
    version: JvmArtifactVersionField
9✔
474
    packages: JvmArtifactPackagesField
9✔
475
    exclusions: ScalaArtifactExclusionsField
9✔
476
    crossversion: ScalaArtifactCrossversionField
9✔
477

478
    required_fields = (
9✔
479
        JvmArtifactGroupField,
480
        ScalaArtifactArtifactField,
481
        JvmArtifactVersionField,
482
        JvmArtifactPackagesField,
483
        ScalaArtifactCrossversionField,
484
    )
485

486

487
class ScalaArtifactTarget(TargetGenerator):
9✔
488
    alias = "scala_artifact"
9✔
489
    help = help_text(
9✔
490
        """
491
        A third-party Scala artifact, as identified by its Maven-compatible coordinate.
492

493
        That is, an artifact identified by its `group`, `artifact`, and `version` components.
494

495
        Each artifact is associated with one or more resolves (a logical name you give to a
496
        lockfile). For this artifact to be used by your first-party code, it must be
497
        associated with the resolve(s) used by that code. See the `resolve` field.
498

499
        Being a Scala artifact, the final artifact name will be inferred using the Scala version
500
        configured for the given resolve.
501
        """
502
    )
503
    core_fields = (
9✔
504
        *COMMON_TARGET_FIELDS,
505
        *ScalaArtifactFieldSet.required_fields,
506
        ScalaArtifactExclusionsField,
507
        JvmArtifactUrlField,
508
        JvmArtifactJarSourceField,
509
        JvmMainClassNameField,
510
    )
511
    copied_fields = (
9✔
512
        *COMMON_TARGET_FIELDS,
513
        JvmArtifactGroupField,
514
        JvmArtifactVersionField,
515
        JvmArtifactPackagesField,
516
        JvmArtifactUrlField,
517
        JvmArtifactJarSourceField,
518
        JvmMainClassNameField,
519
    )
520
    moved_fields = (
9✔
521
        JvmArtifactResolveField,
522
        JvmJdkField,
523
    )
524

525

526
class GenerateJvmArtifactForScalaTargets(GenerateTargetsRequest):
9✔
527
    generate_from = ScalaArtifactTarget
9✔
528

529

530
@rule
9✔
531
async def generate_jvm_artifact_targets(
9✔
532
    request: GenerateJvmArtifactForScalaTargets,
533
    jvm: JvmSubsystem,
534
    scala: ScalaSubsystem,
535
    union_membership: UnionMembership,
536
) -> GeneratedTargets:
537
    field_set = ScalaArtifactFieldSet.create(request.generator)
2✔
538
    resolve_name = request.template.get(JvmArtifactResolveField.alias) or jvm.default_resolve
2✔
539
    scala_version = scala.version_for_resolve(resolve_name)
2✔
540

541
    exclusions_field = {}
2✔
542
    if field_set.exclusions.value:
2✔
543
        exclusions = []
1✔
544
        for exclusion in field_set.exclusions.value:
1✔
545
            if not isinstance(exclusion, ScalaArtifactExclusion):
1✔
546
                exclusions.append(exclusion)
1✔
547
            else:
548
                excluded_artifact_name = None
1✔
549
                if exclusion.artifact:
1✔
550
                    cross_mode = ScalaCrossVersionMode(exclusion.crossversion)
1✔
551
                    excluded_artifact_name = (
1✔
552
                        f"{exclusion.artifact}_{scala_version.crossversion(cross_mode)}"
553
                    )
554
                exclusions.append(
1✔
555
                    JvmArtifactExclusion(group=exclusion.group, artifact=excluded_artifact_name)
556
                )
557
        exclusions_field[JvmArtifactExclusionsField.alias] = exclusions
1✔
558

559
    cross_mode = ScalaCrossVersionMode(
2✔
560
        field_set.crossversion.value or ScalaArtifactCrossversionField.default
561
    )
562
    artifact_name = f"{field_set.artifact.value}_{scala_version.crossversion(cross_mode)}"
2✔
563
    jvm_artifact_target = JvmArtifactTarget(
2✔
564
        {
565
            **request.template,
566
            JvmArtifactArtifactField.alias: artifact_name,
567
            **exclusions_field,
568
        },
569
        request.generator.address.create_generated(artifact_name),
570
        union_membership,
571
        residence_dir=request.generator.address.spec_path,
572
    )
573

574
    return GeneratedTargets(request.generator, (jvm_artifact_target,))
2✔
575

576

577
SCALA_SOURCES_TARGET_TYPES: list[type[Target]] = [
9✔
578
    ScalaSourceTarget,
579
    ScalaSourcesGeneratorTarget,
580
    ScalatestTestTarget,
581
    ScalatestTestsGeneratorTarget,
582
    ScalaJunitTestTarget,
583
    ScalaJunitTestsGeneratorTarget,
584
]
585

586

587
def rules():
9✔
588
    return (
9✔
589
        *collect_rules(),
590
        *jvm_target_types.rules(),
591
        *jvm_run_rules(ScalaFieldSet),
592
        UnionRule(TargetFilesGeneratorSettingsRequest, ScalaSettingsRequest),
593
        UnionRule(GenerateTargetsRequest, GenerateJvmArtifactForScalaTargets),
594
    )
595

596

597
def build_file_aliases():
9✔
598
    return BuildFileAliases(objects={ScalaArtifactExclusion.alias: ScalaArtifactExclusion})
×
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