Skip to content
Closed
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
4 changes: 4 additions & 0 deletions crates/pyrefly_config/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3616,6 +3616,10 @@ output-format = "omit-errors"
errors.severity(ErrorKind::UnknownAttributeType),
Severity::Ignore
);
assert_eq!(
errors.severity(ErrorKind::UnknownAttributeAccess),
Severity::Ignore
);
assert_eq!(
errors.severity(ErrorKind::UnknownVariableType),
Severity::Ignore
Expand Down
3 changes: 3 additions & 0 deletions crates/pyrefly_config/src/error_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,8 @@ pub enum ErrorKind {
/// A call argument whose type is an implicit `Any` (unknown), because the value
/// passed has an unknown type.
UnknownArgumentType,
/// Accessing an attribute whose type is an implicit `Any` (unknown).
UnknownAttributeAccess,
/// An unannotated attribute assigned a value with unknown type.
UnknownAttributeType,
/// Accessing a DataFrame column that does not exist in the inferred schema.
Expand Down Expand Up @@ -622,6 +624,7 @@ impl ErrorKind {
ErrorKind::UnknownArgumentType => Severity::Ignore,
ErrorKind::ImplicitAnyLambda => Severity::Ignore,
ErrorKind::UnknownAttributeType => Severity::Ignore,
ErrorKind::UnknownAttributeAccess => Severity::Ignore,
ErrorKind::UnknownVariableType => Severity::Ignore,
ErrorKind::UnnecessaryComparison => Severity::Warn,
ErrorKind::UnnecessaryTypeConversion => Severity::Warn,
Expand Down
6 changes: 5 additions & 1 deletion crates/pyrefly_config/src/migration/error_codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,11 +310,15 @@ mod tests {

assert!(result.is_ok());
let errors = pyrefly_cfg.root.errors.as_ref().unwrap();
// `reportUnknownMemberType` maps to `unknown-attribute-type`.
// `reportUnknownMemberType` covers both declaring and accessing a member of unknown type.
assert_eq!(
errors.severity(ErrorKind::UnknownAttributeType),
Severity::Error
);
assert_eq!(
errors.severity(ErrorKind::UnknownAttributeAccess),
Severity::Error
);
}

#[test]
Expand Down
4 changes: 4 additions & 0 deletions crates/pyrefly_config/src/migration/pyright.rs
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,10 @@ impl RuleOverrides {
self.report_unknown_member_type,
ErrorKind::UnknownAttributeType,
);
add(
self.report_unknown_member_type,
ErrorKind::UnknownAttributeAccess,
);
add(
self.report_unknown_lambda_type,
ErrorKind::ImplicitAnyLambda,
Expand Down
17 changes: 16 additions & 1 deletion pyrefly/lib/alt/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2492,7 +2492,22 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
x.attr.range,
AttributeReferenceKind::Textual,
);
self.attr_infer(base, &x.attr.id, x.range, errors, None)
let result = self.attr_infer(base, &x.attr.id, x.range, errors, None);
// Like pyright, every access whose type is unknown is reported, including accesses on a
// base that is itself unknown. Missing attributes produce an error `Any` instead, which
// is reported as `missing-attribute`.
if matches!(result.ty(), Type::Any(AnyStyle::Implicit)) {
self.error(
errors,
x.attr.range,
ErrorKind::UnknownAttributeAccess,
format!(
"The type of attribute `{}` is unknown; it is inferred as an implicit `Any`",
x.attr.id
),
);
}
result
}

pub fn attr_infer(
Expand Down
35 changes: 35 additions & 0 deletions pyrefly/lib/test/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3404,3 +3404,38 @@ def use(c: C) -> None:
reveal_type(c.f) # E: revealed type: Unknown
"#,
);

testcase!(
test_unknown_attribute_access,
TestEnv::new().enable_unknown_attribute_access_error(),
r#"
class C:
def __init__(self, v) -> None:
self.known: int = 1
self.unknown = v

def use(obj, c: C) -> None:
obj.attr # E: The type of attribute `attr` is unknown
obj.method(1) # E: The type of attribute `method` is unknown
obj.attr.deeper # E: The type of attribute `attr` is unknown # E: The type of attribute `deeper` is unknown
c.unknown # E: The type of attribute `unknown` is unknown
c.known
"#,
);

testcase!(
test_unknown_attribute_access_no_error,
TestEnv::new().enable_unknown_attribute_access_error(),
r#"
from typing import Any

class C:
x: int = 1

def use(obj, a: Any, c: C) -> None:
a.attr # An explicit `Any` is intentional.
c.missing # E: Object of class `C` has no attribute `missing`
obj.attr = 1 # Assignment targets are not accesses.
c.x
"#,
);
10 changes: 10 additions & 0 deletions pyrefly/lib/test/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ pub struct TestEnv {
empty_body_error: bool,
unknown_argument_type_error: bool,
unknown_variable_type_error: bool,
unknown_attribute_access_error: bool,
implicit_reexport_error: bool,
potential_bad_keyword_argument_error: bool,
default_require_level: Require,
Expand Down Expand Up @@ -246,6 +247,7 @@ impl TestEnv {
empty_body_error: false,
unknown_argument_type_error: false,
unknown_variable_type_error: false,
unknown_attribute_access_error: false,
implicit_reexport_error: false,
potential_bad_keyword_argument_error: false,
default_require_level: Require::Exports,
Expand Down Expand Up @@ -534,6 +536,11 @@ impl TestEnv {
self
}

pub fn enable_unknown_attribute_access_error(mut self) -> Self {
self.unknown_attribute_access_error = true;
self
}

pub fn enable_implicit_reexport_error(mut self) -> Self {
self.implicit_reexport_error = true;
self
Expand Down Expand Up @@ -760,6 +767,9 @@ impl TestEnv {
if self.unknown_variable_type_error {
errors.set_error_severity(ErrorKind::UnknownVariableType, Severity::Error);
}
if self.unknown_attribute_access_error {
errors.set_error_severity(ErrorKind::UnknownAttributeAccess, Severity::Error);
}
config.extra_file_extensions = self.extra_file_extensions.clone();
let mut sourcedb = MapDatabase::new(config.get_sys_info());
for (name, path, _) in self.modules.iter() {
Expand Down
1 change: 1 addition & 0 deletions scripts/error_presets.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
"unimported-directive": ["legacy", "default", "strict", "all"],
"uninitialized-instance-variable": ["all"],
"unknown-argument-type": ["all"],
"unknown-attribute-access": ["all"],
"unknown-attribute-type": ["all"],
"unknown-column": ["legacy", "default", "strict", "all"],
"unknown-name": ["basic", "legacy", "default", "strict", "all"],
Expand Down
21 changes: 21 additions & 0 deletions website/docs/error-kinds.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2048,6 +2048,27 @@ def f(n: int) -> None: ...
f(untyped(1)) # the argument's type is unknown [unknown-argument-type]
```

## unknown-attribute-access

Default severity: `ignore`

An attribute is accessed whose type is an implicit `Any` (unknown), so uses of the result are not checked. This mirrors pyright's `reportUnknownMemberType` for member accesses. Every such access is reported, including accesses on a value that is itself unknown, so a chain like `obj.a.b` on an unannotated parameter reports both `a` and `b`. Accesses on an explicit `Any` are treated as intentional and are not reported.

Compare this with [unknown-attribute-type](#unknown-attribute-type), which is reported where an unannotated attribute is assigned a value of unknown type.

```python
def untyped(x): # unannotated: returns an implicit `Any`
return x

class C:
def __init__(self) -> None:
self.value = untyped(1)

def use(c: C, obj) -> None:
c.value # the type of attribute `value` is unknown [unknown-attribute-access]
obj.attr.deeper # both `attr` and `deeper` are unknown [unknown-attribute-access]
```

## unknown-attribute-type

Default severity: `ignore`
Expand Down
2 changes: 1 addition & 1 deletion website/docs/migrate/pyright/diagnostics-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ several kinds.
| `reportUnknownArgumentType` | `unknown-argument-type` | Automatic | Direct | |
| `reportUnknownLambdaType` | `implicit-any-lambda` | Automatic | Direct-ish | |
| `reportUnknownVariableType` | `unknown-variable-type` | Automatic | Direct | |
| `reportUnknownMemberType` | `unknown-attribute-type` | Automatic | Direct-ish | |
| `reportUnknownMemberType` | `unknown-attribute-access`, `unknown-attribute-type` | Automatic | Direct-ish | |
| `reportMissingParameterType` | `implicit-any-parameter` | Automatic | Direct-ish | |
| `reportMissingTypeArgument` | `implicit-any-type-argument` | Automatic | Direct-ish | |
| `reportInvalidTypeVarUse` | `invalid-type-var` | Automatic | Broad | Some variance-specific cases use `invalid-variance` or `variance-mismatch`. |
Expand Down
24 changes: 16 additions & 8 deletions website/docs/migrate/pyright/unknown-member-type.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
title: Pyright reportUnknownMemberType in Pyrefly
slug: /migrate/pyright/unknown-member-type
description:
Map Pyright reportUnknownMemberType to Pyrefly unknown-attribute-type and
distinguish unknown member types from missing attributes.
Map Pyright reportUnknownMemberType to Pyrefly unknown-attribute-access and
unknown-attribute-type, and distinguish unknown member types from missing
attributes.
keywords:
- reportUnknownMemberType
- pyright unknown member type
- pyright unknown attribute
- unknown-attribute-type
- unknown-attribute-access
---

{/*
Expand All @@ -19,16 +21,21 @@ keywords:
*/}

Pyright's `reportUnknownMemberType` fires when a member exists but its type
cannot be determined. `pyrefly init` maps it to
[`unknown-attribute-type`](../../error-kinds.mdx#unknown-attribute-type):
cannot be determined, both where the member is accessed and where it is
assigned. Pyrefly splits these into two kinds, and `pyrefly init` maps the rule
to both:
[`unknown-attribute-access`](../../error-kinds.mdx#unknown-attribute-access) for accesses
and [`unknown-attribute-type`](../../error-kinds.mdx#unknown-attribute-type) for
assignments of an unknown value to an unannotated attribute.

```toml
[errors]
unknown-attribute-access = "error"
unknown-attribute-type = "error"
```

The Pyrefly kind defaults to `ignore`, so a project that relied on Pyright's
strict unknown-member policy has to enable it explicitly. `pyrefly init` does
Both kinds default to `ignore`, so a project that relied on Pyright's strict
unknown-member policy has to enable them explicitly. `pyrefly init` does
that for you when the Pyright config sets the rule.

## Unknown is not the same as missing
Expand All @@ -46,7 +53,8 @@ Pyrefly reports these as different kinds:

| Scenario | Pyrefly kind |
| --------------------------------------------------------------------------------------------------- | -------------------------- |
| The attribute exists but its inferred type is unknown | `unknown-attribute-type` |
| An attribute whose type is unknown is accessed | `unknown-attribute-access` |
| An unannotated attribute is assigned a value of unknown type | `unknown-attribute-type` |
| The attribute does not exist | `missing-attribute` |
| An imported name is absent from a module | `missing-module-attribute` |
| The attribute is implicitly `Any`-like because it was assigned `None` or `()` without an annotation | `implicit-any-attribute` |
Expand All @@ -58,7 +66,7 @@ policy rather than preserving it.

| Pyright | Pyrefly |
| ---------------------------- | ------------------------ |
| `reportUnknownMemberType` | `unknown-attribute-type` |
| `reportUnknownMemberType` | `unknown-attribute-access`, `unknown-attribute-type` |
| `reportUnknownVariableType` | `unknown-variable-type` |
| `reportUnknownArgumentType` | `unknown-argument-type` |
| `reportUnknownParameterType` | `implicit-any-parameter` |
Expand Down
Loading