Repository: pantsbuild/pants

Issue:
Deprecated fields not preserved for target generators
**Describe the bug**
When a target field has a deprecated alias, it is not propagated to the generated target when using the deprecated alias, nor is there a warning about using the deprecated name for the field.

**Pants version**
`main`

**OS**
Any

**Additional info**
As found by @krishnan-chandra and identified by @kaos in https://github.com/pantsbuild/pants/pull/20626#discussion_r1505851913




Discussion:

Resolving patch:
diff --git a/src/python/pants/engine/internals/graph.py b/src/python/pants/engine/internals/graph.py
--- a/src/python/pants/engine/internals/graph.py
+++ b/src/python/pants/engine/internals/graph.py
@@ -11,7 +11,7 @@
 import os.path
 from dataclasses import dataclass
 from pathlib import PurePath
-from typing import Any, Iterable, Iterator, NamedTuple, Sequence, Type, cast
+from typing import Any, Iterable, Iterator, NamedTuple, Sequence, Type, TypeVar, cast
 
 from pants.base.deprecated import warn_or_error
 from pants.base.specs import AncestorGlobSpec, RawSpecsWithoutFileOwners, RecursiveGlobSpec
@@ -303,12 +303,12 @@ async def resolve_generator_target_requests(
         generator_fields,
         union_membership,
     )
-    base_generator = target_type(
-        generator_fields,
+    base_generator = _create_target(
         req.address,
-        name_explicitly_set=target_adaptor.name_explicitly_set,
-        union_membership=union_membership,
-        description_of_origin=target_adaptor.description_of_origin,
+        target_type,
+        target_adaptor,
+        generator_fields,
+        union_membership,
     )
     overrides = await _target_generator_overrides(base_generator, unmatched_build_file_globs)
     return ResolvedTargetGeneratorRequests(
@@ -362,6 +362,36 @@ async def resolve_target_parametrizations(
     return _TargetParametrizations(parametrizations)
 
 
+_TargetType = TypeVar("_TargetType", bound=Target)
+
+
+def _create_target(
+    address: Address,
+    target_type: type[_TargetType],
+    target_adaptor: TargetAdaptor,
+    field_values: dict[str, Any],
+    union_membership: UnionMembership,
+    name_explicitly_set: bool | None = None,
+) -> _TargetType:
+    target = target_type(
+        field_values,
+        address,
+        name_explicitly_set=(
+            target_adaptor.name_explicitly_set
+            if name_explicitly_set is None
+            else name_explicitly_set
+        ),
+        union_membership=union_membership,
+        description_of_origin=target_adaptor.description_of_origin,
+    )
+    # Check for any deprecated field usage.
+    for field_type in target.field_types:
+        if field_type.deprecated_alias is not None and field_type.deprecated_alias in field_values:
+            warn_deprecated_field_type(field_type)
+
+    return target
+
+
 def _target_parametrizations(
     address: Address,
     target_adaptor: TargetAdaptor,
@@ -375,12 +405,12 @@ def _target_parametrizations(
         generated = FrozenDict(
             (
                 parameterized_address,
-                target_type(
-                    parameterized_fields,
+                _create_target(
                     parameterized_address,
-                    name_explicitly_set=target_adaptor.name_explicitly_set,
-                    union_membership=union_membership,
-                    description_of_origin=target_adaptor.description_of_origin,
+                    target_type,
+                    target_adaptor,
+                    parameterized_fields,
+                    union_membership,
                 ),
             )
             for parameterized_address, parameterized_fields in expanded_parametrizations
@@ -388,19 +418,13 @@ def _target_parametrizations(
         return _TargetParametrization(None, generated)
     else:
         # The target was not parametrized.
-        target = target_type(
-            target_adaptor.kwargs,
+        target = _create_target(
             address,
-            name_explicitly_set=target_adaptor.name_explicitly_set,
-            union_membership=union_membership,
-            description_of_origin=target_adaptor.description_of_origin,
+            target_type,
+            target_adaptor,
+            target_adaptor.kwargs,
+            union_membership,
         )
-        for field_type in target.field_types:
-            if (
-                field_type.deprecated_alias is not None
-                and field_type.deprecated_alias in target_adaptor.kwargs
-            ):
-                warn_deprecated_field_type(field_type)
         return _TargetParametrization(target, FrozenDict())
 
 
@@ -422,10 +446,20 @@ def _parametrized_target_generators_with_templates(
         *target_type._find_moved_plugin_fields(union_membership),
     )
     for field_type in copied_fields:
-        field_value = generator_fields.get(field_type.alias, None)
-        if field_value is not None:
-            template_fields[field_type.alias] = field_value
+        for alias in (field_type.deprecated_alias, field_type.alias):
+            if alias is None:
+                continue
+            # Any deprecated field use will be checked on the generator target.
+            field_value = generator_fields.get(alias, None)
+            if field_value is not None:
+                template_fields[alias] = field_value
     for field_type in moved_fields:
+        # We must check for deprecated field usage here before passing the value to the generator.
+        if field_type.deprecated_alias is not None:
+            field_value = generator_fields.pop(field_type.deprecated_alias, None)
+            if field_value is not None:
+                warn_deprecated_field_type(field_type)
+                template_fields[field_type.deprecated_alias] = field_value
         field_value = generator_fields.pop(field_type.alias, None)
         if field_value is not None:
             template_fields[field_type.alias] = field_value
@@ -459,12 +493,13 @@ def _parametrized_target_generators_with_templates(
         )
     return [
         (
-            target_type(
-                generator_fields,
+            _create_target(
                 address,
+                target_type,
+                target_adaptor,
+                generator_fields,
+                union_membership,
                 name_explicitly_set=target_adaptor.name is not None,
-                union_membership=union_membership,
-                description_of_origin=target_adaptor.description_of_origin,
             ),
             template,
         )


Test patch:
diff --git a/src/python/pants/engine/internals/graph_test.py b/src/python/pants/engine/internals/graph_test.py
--- a/src/python/pants/engine/internals/graph_test.py
+++ b/src/python/pants/engine/internals/graph_test.py
@@ -13,6 +13,7 @@
 
 import pytest
 
+from pants.base.deprecated import warn_or_error
 from pants.base.specs import Specs
 from pants.base.specs_parser import SpecsParser
 from pants.engine.addresses import Address, Addresses, AddressInput, UnparsedAddressInputs
@@ -27,6 +28,7 @@
     _DependencyMapping,
     _DependencyMappingRequest,
     _TargetParametrizations,
+    warn_deprecated_field_type,
 )
 from pants.engine.internals.native_engine import AddressParseException
 from pants.engine.internals.parametrize import Parametrize, _TargetParametrizationsRequest
@@ -73,6 +75,7 @@
 )
 from pants.engine.unions import UnionMembership, UnionRule
 from pants.testutil.rule_runner import QueryRule, RuleRunner, engine_error
+from pants.util.docutil import bin_name
 from pants.util.ordered_set import FrozenOrderedSet
 
 
@@ -114,7 +117,8 @@ def resolve_field_default_factory(
 
 
 class MockMultipleSourcesField(MultipleSourcesField):
-    pass
+    deprecated_alias = "deprecated_sources"
+    deprecated_alias_removal_version = "9.9.9.dev0"
 
 
 class MockPluginField(StringField):
@@ -146,7 +150,7 @@ class MockTargetGenerator(TargetFilesGenerator):
     core_fields = (MockMultipleSourcesField, OverridesField)
     generated_target_cls = MockGeneratedTarget
     copied_fields = ()
-    moved_fields = (Dependencies, Tags, ResolveField)
+    moved_fields = (MockDependencies, Tags, ResolveField)
 
 
 @pytest.fixture
@@ -702,6 +706,32 @@ def test_deprecated_field_name(transitive_targets_rule_runner: RuleRunner, caplo
     assert "Instead, use `dependencies`" in caplog.text
 
 
+def test_deprecated_field_name_on_generator_issue_20627(
+    transitive_targets_rule_runner: RuleRunner, caplog
+) -> None:
+    warn_deprecated_field_type.forget(MockDependencies)  # type: ignore[attr-defined]
+    warn_or_error.forget(  # type: ignore[attr-defined]
+        removal_version=MockDependencies.deprecated_alias_removal_version,
+        entity=f"the field name {MockDependencies.deprecated_alias}",
+        hint=(
+            f"Instead, use `{MockDependencies.alias}`, which behaves the same. Run `{bin_name()} "
+            "update-build-files` to automatically fix your BUILD files."
+        ),
+    )
+    transitive_targets_rule_runner.write_files(
+        {
+            "f1.txt": "",
+            "BUILD": "generator(name='t', deprecated_sources=['f1.txt'], deprecated_field=[])",
+        }
+    )
+    transitive_targets_rule_runner.get_target(
+        Address("", target_name="t", relative_file_path="f1.txt")
+    )
+    assert len(caplog.records) == 2
+    assert "Instead, use `sources`" in caplog.text
+    assert "Instead, use `dependencies`" in caplog.text
+
+
 def test_resolve_deprecated_target_name(transitive_targets_rule_runner: RuleRunner, caplog) -> None:
     transitive_targets_rule_runner.write_files({"BUILD": "deprecated_target(name='t')"})
     transitive_targets_rule_runner.get_target(Address("", target_name="t"))