test: document enum semantic-token classification bugs (#5050)

Summary:
Semantic tokens classify attributes from their inferred value types instead of their definitions. Snapshots capture the current misclassification, with comments identifying the correct enumMember or property token for each affected case.

These LSP tests use ordinary Rust `#[test]` functions, so explicit `BUG` comments document the incorrect behavior and the correct result. These are passing tests of known bugs, following [the repository test guidance](https://github.com/facebook/pyrefly/blob/main/AGENTS.md#the-bug-marker-in-tests).

The complete implementation and regression tests remain in [PR https://github.com/facebook/pyrefly/issues/4794](https://github.com/facebook/pyrefly/pull/4794). Related issue: https://github.com/facebook/pyrefly/issues/4751.

## Validation

Based on upstream `main` at `ef6de648bce0dc6dcca0a21a9fbb7490f23ecce8`.

- `cargo test -p pyrefly --lib -- enum_member_definition_test narrowed_enum_instance_attribute_test` — passed.
- `python3 test.py --no-test --no-tensor-shapes --no-conformance --no-jsonschema --no-extension` — passed.

Pull Request resolved: https://github.com/facebook/pyrefly/pull/5050

Reviewed By: rchen152

Differential Revision: D122390462

fbshipit-source-id: 79027859a79c700aca570d74df86de37bf3e66de
diff --git a/pyrefly/lib/test/lsp/semantic_tokens.rs b/pyrefly/lib/test/lsp/semantic_tokens.rs
index b5879ec..f31baa2 100644
--- a/pyrefly/lib/test/lsp/semantic_tokens.rs
+++ b/pyrefly/lib/test/lsp/semantic_tokens.rs
@@ -1193,6 +1193,167 @@
     );
 }
 
+// BUG: Enum highlighting follows the inferred value type, not the attribute definition.
+// union_class.A should be enumMember. Holder.kind, Holder.A,
+// holder.kind, and e.kind should be property (issue #4751).
+#[test]
+fn enum_member_definition_test() {
+    let mut env = TestEnv::new();
+    env.add(
+        "fixtures",
+        r#"
+from enum import Enum
+from typing import ClassVar, Literal
+
+class E(Enum):
+    A = 1
+    ALIAS = A
+
+    @property
+    def kind(self) -> Literal[E.A]:
+        return E.A
+
+class F(Enum):
+    A = 1
+
+class Holder:
+    kind: ClassVar[Literal[E.A]] = E.A
+    A: ClassVar[Literal[E.A]] = E.A
+
+holder: Holder
+e: E
+enum_class: type[E]
+union_class: type[E] | type[F]
+mixed_class: type[E] | type[Holder]
+"#,
+    );
+    for (expression, receiver_kind, attribute_kind) in [
+        ("E.A", "class", "enumMember"),
+        ("E.ALIAS", "class", "enumMember"),
+        ("e.A", "variable", "enumMember"),
+        ("enum_class.A", "class", "enumMember"),
+        ("union_class.A", "class", "property"),
+        ("mixed_class.A", "class", "property"),
+        ("Holder.kind", "class", "enumMember"),
+        ("Holder.A", "class", "enumMember"),
+        ("holder.kind", "variable", "enumMember"),
+        ("e.kind", "variable", "enumMember"),
+    ] {
+        let (receiver, attribute) = expression.split_once('.').unwrap();
+        let code = format!("from fixtures import *\n{expression}\n");
+        let expected = format!(
+            "# main.py
+line: 0, column: 5, length: 8, text: fixtures
+token-type: namespace
+
+line: 0, column: 21, length: 1, text: *
+token-type: namespace
+
+line: 1, column: 0, length: {}, text: {receiver}
+token-type: {receiver_kind}
+
+line: 1, column: {}, length: {}, text: {attribute}
+token-type: {attribute_kind}",
+            receiver.len(),
+            receiver.len() + 1,
+            attribute.len(),
+        );
+        assert_full_semantic_tokens_with_syntax_and_env(
+            &[("main", &code)],
+            false,
+            env.clone(),
+            &expected,
+        );
+    }
+}
+
+// BUG: Narrowing changes the highlighting of holder.kind to enumMember.
+// Both occurrences should remain property; E.A and E.B should be enumMember (issue #4751).
+#[test]
+fn narrowed_enum_instance_attribute_test() {
+    let code = r#"
+from enum import Enum
+
+class E(Enum):
+    A = 1
+    B = 2
+
+class Holder:
+    kind: E
+
+def f(holder: Holder) -> None:
+    if holder.kind is E.A:
+        pass
+    elif holder.kind is E.B:
+        pass
+"#;
+    assert_full_semantic_tokens(
+        &[("main", code)],
+        r#"
+# main.py
+line: 1, column: 5, length: 4, text: enum
+token-type: namespace
+
+line: 1, column: 17, length: 4, text: Enum
+token-type: class
+
+line: 3, column: 6, length: 1, text: E
+token-type: class
+
+line: 3, column: 8, length: 4, text: Enum
+token-type: class
+
+line: 4, column: 4, length: 1, text: A
+token-type: variable, token-modifiers: [readonly]
+
+line: 5, column: 4, length: 1, text: B
+token-type: variable, token-modifiers: [readonly]
+
+line: 7, column: 6, length: 6, text: Holder
+token-type: class
+
+line: 8, column: 4, length: 4, text: kind
+token-type: variable
+
+line: 8, column: 10, length: 1, text: E
+token-type: class
+
+line: 10, column: 4, length: 1, text: f
+token-type: function
+
+line: 10, column: 6, length: 6, text: holder
+token-type: parameter
+
+line: 10, column: 14, length: 6, text: Holder
+token-type: class
+
+line: 11, column: 7, length: 6, text: holder
+token-type: parameter
+
+line: 11, column: 14, length: 4, text: kind
+token-type: property
+
+line: 11, column: 22, length: 1, text: E
+token-type: class
+
+line: 11, column: 24, length: 1, text: A
+token-type: enumMember
+
+line: 13, column: 9, length: 6, text: holder
+token-type: parameter
+
+line: 13, column: 16, length: 4, text: kind
+token-type: enumMember
+
+line: 13, column: 24, length: 1, text: E
+token-type: class
+
+line: 13, column: 26, length: 1, text: B
+token-type: enumMember
+"#,
+    );
+}
+
 #[test]
 fn type_alias_test() {
     let code = r#"