Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions conformance/third_party/conformance.exp
Original file line number Diff line number Diff line change
Expand Up @@ -5075,6 +5075,39 @@
}
],
"enums_member_values.py": [
{
"code": -2,
"column": 12,
"concise_description": "assert_type(int, Literal[1]) failed",
"description": "assert_type(int, Literal[1]) failed",
"line": 21,
"name": "assert-type",
"severity": "error",
"stop_column": 43,
"stop_line": 21
},
{
"code": -2,
"column": 12,
"concise_description": "assert_type(int, Literal[1]) failed",
"description": "assert_type(int, Literal[1]) failed",
"line": 22,
"name": "assert-type",
"severity": "error",
"stop_column": 41,
"stop_line": 22
},
{
"code": -2,
"column": 16,
"concise_description": "assert_type(int, Literal[1, 3]) failed",
"description": "assert_type(int, Literal[1, 3]) failed",
"line": 26,
"name": "assert-type",
"severity": "error",
"stop_column": 50,
"stop_line": 26
},
{
"code": -2,
"column": 16,
Expand Down Expand Up @@ -5111,8 +5144,8 @@
{
"code": -2,
"column": 5,
"concise_description": "Enum member `GREEN` has type `Literal['green']`, must match the `_value_` attribute annotation of `int`",
"description": "Enum member `GREEN` has type `Literal['green']`, must match the `_value_` attribute annotation of `int`",
"concise_description": "Enum member `GREEN` has type `str`, must match the `_value_` attribute annotation of `int`",
"description": "Enum member `GREEN` has type `str`, must match the `_value_` attribute annotation of `int`",
"line": 78,
"name": "bad-assignment",
"severity": "error",
Expand Down
13 changes: 10 additions & 3 deletions crates/pyrefly_config/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -730,7 +730,10 @@ impl ConfigFile {
}

pub fn from_real_config_file(&self) -> bool {
matches!(self.source, ConfigSource::File(_))
matches!(
self.source,
ConfigSource::File(_) | ConfigSource::FailedParse(_)
)
}

pub fn python_version(&self) -> PythonVersion {
Expand Down Expand Up @@ -1225,7 +1228,7 @@ impl ConfigFile {

let mut configure_source_db = |build_system: &mut BuildSystem| {
let root = match &self.source {
ConfigSource::File(path) => {
ConfigSource::File(path) | ConfigSource::FailedParse(path) => {
let mut root = path.to_path_buf();
root.pop();
root
Expand Down Expand Up @@ -1278,7 +1281,7 @@ impl ConfigFile {
));
}

if let ConfigSource::File(path) = &self.source {
if let ConfigSource::File(path) | ConfigSource::FailedParse(path) = &self.source {
configure_errors
.into_map(|e| ConfigError::warn(e.context(format!("{}", path.display()))))
} else {
Expand Down Expand Up @@ -2490,6 +2493,10 @@ output-format = "omit-errors"
"Expected FailedParse, got {:?}",
config.source
);
assert!(
config.from_real_config_file(),
"FailedParse config should be treated as a real config file"
);
assert!(!errors.is_empty(), "Expected errors for invalid TOML");
// The config should still respect the file's location for project root detection.
assert_eq!(config.source.root(), Some(root.path()));
Expand Down
15 changes: 1 addition & 14 deletions crates/pyrefly_types/src/literal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ use pyrefly_derive::TypeEq;
use pyrefly_derive::Visit;
use pyrefly_derive::VisitMut;
use pyrefly_util::assert_words;
use pyrefly_util::visit::VisitMut;
use ruff_python_ast::ExprBooleanLiteral;
use ruff_python_ast::ExprBytesLiteral;
use ruff_python_ast::ExprFString;
Expand Down Expand Up @@ -58,27 +57,15 @@ pub enum Lit {
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(Visit, TypeEq)]
#[derive(Visit, VisitMut, TypeEq)]
pub struct LitEnum {
pub class: ClassType,
pub member: Name,
/// Raw type assigned to name in class def.
/// We store the raw type so we can return it when the value or _value_ attribute is accessed.
/// NOTE: Intentionally excluded from VisitMut so that type transformations like
/// `promote_implicit_literals` don't mutate this metadata field, which must remain
/// stable for `.value` resolution.
pub ty: Type,
}

/// Manual VisitMut that skips the `ty` field. The `ty` is semantic metadata for `.value`
/// resolution and must not be mutated by recursive type transformations (e.g. literal promotion).
/// We still recurse into `class` so that type arguments on generic enums are visited.
impl VisitMut<Type> for LitEnum {
fn recurse_mut(&mut self, f: &mut dyn FnMut(&mut Type)) {
self.class.visit_mut(f);
}
}

impl Display for Lit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Expand Down
34 changes: 10 additions & 24 deletions pyrefly/lib/alt/class/class_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1762,27 +1762,25 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
// Determine the final type, promoting literals when appropriate.
// Skip literal promotion for NNModule types: their fields are captured
// constructor args that must preserve literal types for shape inference.
let (ty, unpromoted_ty) = if matches!(value_ty, Type::NNModule(_)) {
(value_ty, None)
let ty = if matches!(value_ty, Type::NNModule(_)) {
value_ty
} else {
let mut has_implicit_literal = value_ty.is_implicit_literal();
if !has_implicit_literal && matches!(initialization, ClassFieldInitialization::Method) {
value_ty.universe(&mut |current_type_node| {
has_implicit_literal |= current_type_node.is_implicit_literal();
});
}
// Save any unpromoted literal types, we need them for enums
if annotation
.as_ref()
.and_then(|ann| ann.ty.as_ref())
.is_none()
&& matches!(read_only_reason, None | Some(ReadOnlyReason::NamedTuple))
&& has_implicit_literal
{
let pre = value_ty.clone();
(value_ty.promote_implicit_literals(self.stdlib), Some(pre))
value_ty.promote_implicit_literals(self.stdlib)
} else {
(value_ty, None)
value_ty
}
};

Expand Down Expand Up @@ -1866,7 +1864,6 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
name,
direct_annotation.as_ref(),
&ty,
unpromoted_ty.as_ref(),
field_definition,
descriptor.is_some(),
range,
Expand Down Expand Up @@ -2108,7 +2105,6 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
name: &Name,
direct_annotation: Option<&Annotation>,
ty: &Type,
unpromoted_ty: Option<&Type>,
field_definition: &ClassFieldDefinition,
is_descriptor: bool,
range: TextRange,
Expand All @@ -2118,7 +2114,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
class,
name,
direct_annotation,
unpromoted_ty.unwrap_or(ty),
ty,
field_definition,
is_descriptor,
range,
Expand Down Expand Up @@ -3997,21 +3993,11 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
field: &Name,
ancestor: (&str, &str),
) -> bool {
self.field_defining_class_matches(cls, field, |c| {
c.has_toplevel_qname(ancestor.0, ancestor.1)
})
}

/// Check whether the defining class of `field` on `cls` satisfies `predicate`.
/// Returns false if the field does not exist.
pub fn field_defining_class_matches(
&self,
cls: &Class,
field: &Name,
predicate: impl FnOnce(&Class) -> bool,
) -> bool {
self.get_class_member_with_defining_class(cls, field)
.is_some_and(|member| predicate(&member.defining_class))
let member = self.get_class_member_with_defining_class(cls, field);
match member {
Some(member) => member.is_defined_on(ancestor.0, ancestor.1),
None => false,
}
}

/// Get the class's `__new__` method.
Expand Down
Loading
Loading