diff --git a/pyrefly/lib/alt/class/class_field.rs b/pyrefly/lib/alt/class/class_field.rs index 73288304bf..fbc6ed00c3 100644 --- a/pyrefly/lib/alt/class/class_field.rs +++ b/pyrefly/lib/alt/class/class_field.rs @@ -4333,6 +4333,22 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { else { continue; }; + // Django gives a model without its own primary key a synthesized `id: int`, + // including abstract bases like `AbstractBaseUser` (stubs omit `Meta.abstract`, + // so we can't tell them apart). Django only adds that field to the concrete model + // when no pk is declared, so a child declaring `id` as its primary key replaces it + // rather than overriding it. An explicitly declared parent `id` is still checked. + if field_name.as_str() == "id" + && metadata + .django_model_metadata() + .and_then(|dm| dm.custom_primary_key_field.as_ref()) + == Some(field_name) + && self + .get_non_synthesized_class_member(parent_cls, field_name) + .is_none() + { + continue; + } // `__call__` is checked against a Protocol parent only, unless the user // opts in with `@override`. Implementing a callable interface is // ubiquitous - argparse actions, auth handlers, metaclasses - and diff --git a/pyrefly/lib/test/django/auto_generated_fields.rs b/pyrefly/lib/test/django/auto_generated_fields.rs index 749ef9d1a6..a3c9889eb5 100644 --- a/pyrefly/lib/test/django/auto_generated_fields.rs +++ b/pyrefly/lib/test/django/auto_generated_fields.rs @@ -165,3 +165,45 @@ assert_type(obj.id, str) assert_type(obj.pk, str) "#, ); + +// Abstract bases without their own pk get a synthesized `id: int`. A child that +// declares its own `id` primary key should not be flagged as overriding it. +// https://github.com/meta/pyrefly/issues/4596 +django_testcase!( + test_custom_id_pk_overrides_synthesized_id, + r#" +from typing import assert_type +import uuid +from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin +from django.db import models + +class User(AbstractBaseUser, PermissionsMixin): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + +class Base(models.Model): + class Meta: + abstract = True + +class Child(Base): + id = models.CharField(max_length=36, primary_key=True) + +assert_type(User().id, uuid.UUID) +assert_type(User().pk, uuid.UUID) +assert_type(Child().id, str) +assert_type(Child().pk, str) +"#, +); + +// An `id` that is not the primary key is still checked against the synthesized one. +django_testcase!( + test_non_pk_id_still_checked_against_synthesized_id, + r#" +from django.db import models + +class Base(models.Model): + pass + +class Child(Base): + id = models.CharField(max_length=36) # E: `Child.id` overrides parent class `Base` +"#, +);