Skip to content

Offer TypedDict keys as completions after a dot - #5480

Open
MannXo wants to merge 2 commits into
meta:mainfrom
MannXo:feat/5478-typeddict-dot-completion
Open

MannXo wants to merge 2 commits into
meta:mainfrom
MannXo:feat/5478-typeddict-dot-completion

Conversation

@MannXo

@MannXo MannXo commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #5478

Typing a. on a TypedDict instance now also offers the TypedDict's keys. Accepting one rewrites the access into a subscript, so a. followed by f1 becomes a["f1"].

Each key item has a text edit that replaces everything from the end of the base expression through the attribute name (the dot and any typed prefix) with ["key"]. Its filterText starts with that same text (.f1), because clients match the filter text against what was typed from the start of the edit, and that includes the dot. fastapi_endpoint_support.rs uses the same pattern.

The keys come from collect_typed_dict_keys, which subscript key completion already uses, so inherited, NotRequired and ReadOnly keys are all included. The regular attribute completions (get, keys, ...) are still offered next to them. Keys are quoted with serde_json::to_string, which always gives a valid Python string literal, so functional-syntax keys like "two words" work too. The items are skipped when the base and the attribute are on different lines, since a completion text edit has to stay on one line.

Two behaviours you may want changed:

  1. Keys are offered whenever any member of the base type is a TypedDict, so A | None gets them too. This matches what a[ already does.
  2. Key items use CompletionItemKind::Field with the value type as detail, the same as subscript key completion. They sort alongside the attributes by name.
  • I am a human, and I fully understand the code I am submitting.

Test Plan

Added five tests in pyrefly/lib/test/lsp/completion.rs. They cover the issue example (exact range, newText and filterText, with dict methods still present), a typed prefix (a.fi), inherited/NotRequired/ReadOnly keys, quoting of non-identifier keys, and no rewrite items for a plain class instance or the TypedDict class object. The first commit adds only the tests. Four of them fail on it, and the fifth is a negative check that passes either way.

cargo test -p pyrefly --lib typed_dict_attribute_completion
test result: ok. 5 passed; 0 failed

cargo fmt -- --check                  # clean
cargo clippy --release                # 0 warnings
cargo test --profile bench            # pyrefly lib: 9896 passed, 0 failed, 3 ignored; all other crates pass
cargo check -p pyrefly_wasm --target wasm32-unknown-unknown   # builds
python3 conformance/conformance_output.py conformance/third_party --mode check --executable target/release/pyrefly   # clean
python3 test.py --no-test --no-tensor-shapes --no-conformance --no-jsonschema --no-extension   # clean

I also ran target/release/pyrefly lsp (from the bench-profile build above) over stdio with the issue's example and requested completion right after a.:

{"detail": "int", "filterText": ".f1", "kind": 5, "label": "f1", "sortText": "0.9999.f1", "textEdit": {"newText": "[\"f1\"]", "range": {"end": {"character": 2, "line": 7}, "start": {"character": 1, "line": 7}}}}
{"detail": "int", "filterText": ".f2", "kind": 5, "label": "f2", "sortText": "0.9999.f2", "textEdit": {"newText": "[\"f2\"]", "range": {"end": {"character": 2, "line": 7}, "start": {"character": 1, "line": 7}}}}

copy, get, items, keys, pop, setdefault, update and values came back in the same response. I did not run the Scrut or tensor-shape suites, and I only tested on macOS.

Typing `a.` on a TypedDict instance offers only the mapping methods, so
the keys can only be discovered by switching to `a["`. These tests pin
the behavior requested in meta#5478. Each key is offered after the dot, and
accepting it rewrites the access into `a["key"]`, replacing the dot and
any typed prefix. Inherited and NotRequired/ReadOnly keys are included,
keys that are not identifiers are quoted correctly, dict methods still
appear, and plain classes and the TypedDict class object get no
rewriting items.
TypedDict keys can only be read with a subscript, so after `a.` users
saw the mapping methods but none of the keys they usually want (meta#5478).

Attribute completion now also offers each key of a TypedDict base. The
item carries a text edit that replaces the dot and any typed prefix with
`["key"]`, so accepting `f1` after `a.` produces `a["f1"]`. The keys
come from the same TypedDict field lookup that subscript key completion
uses, so inherited, NotRequired and ReadOnly keys are all included, and
the regular attribute completions are still added alongside them.

The filter text starts with the text the edit replaces (the dot), since
clients match it against what was typed from the start of the edit.
Keys are quoted with JSON string escaping, which is also a valid Python
string literal, so keys that are not identifiers stay correct. The
items are skipped when the base and attribute are on different lines,
because a completion text edit must stay on one line.
@meta-cla meta-cla Bot added the cla signed label Nov 1, 2026
@github-actions github-actions Bot added the size/l label Nov 1, 2026
@github-actions

This comment has been minimized.

@meta-codesync

meta-codesync Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

This pull request has been imported. If you are a Meta employee, you can view this in D128918542. (Because this pull request was imported automatically, there will not be any future comments.)

@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown

According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅

@stroxler stroxler self-assigned this Nov 1, 2026

@stroxler stroxler left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

Overall this looks pretty good to me. AI code review caught a few edge cases we should clean up before merging though:

  1. Parenthesized receivers produce broken syntax — [dict_completions.rs:1112–1113](

    let edit_range =
    module_info.to_lsp_range(TextRange::new(base_range.end(), attribute.range().end()));
    )
    Accepting field after (a).fi produces (a["field"], deleting the closing parenthesis. base_range excludes grouping parentheses, so its end is not necessarily immediately before the dot. Start the edit at the actual attribute dot, preserving parentheses.

  2. Rewriting inside f-strings breaks Python <3.12 — [dict_completions.rs:1124–1132](

    let quoted_key = to_string(&key).expect("Serializing a string cannot fail");
    completions.push(RankedCompletion::new(CompletionItem {
    filter_text: Some(format!("{accessor}{key}")),
    label: key,
    detail: Some(ty.to_string()),
    kind: Some(CompletionItemKind::Field),
    text_edit: Some(CompletionItemTextEdit::TextEdit(TextEdit {
    range: edit_range,
    new_text: format!("[{quoted_key}]"),
    )
    Completing inside f"{a.fi}" produces f"{a["field"]}". JSON quoting is valid for a standalone Python string, but this is invalid before PEP 701. Choose quoting compatible with the enclosing f-string, or suppress unsafe rewrites on older Python versions.

  3. Value patterns receive invalid subscript rewrites — [completion.rs:1227–1233](

    self.add_typed_dict_attribute_key_completions(
    handle,
    &base_type,
    base_range,
    &identifier,
    &mut result,
    );
    )
    Completing case a.fi: produces case a["field"]:, but subscripts are not allowed in patterns. Skip these items in PatternMatchValue contexts.

Otherwise, the feature placement is sensible and the non-TypedDict fast path stays cheap.

Verification: Codex and Claude independently reviewed the change. Existing tests passed; additional applied-edit probes reproduced all three issues. Rust check, build, and lint passed. Validation was Linux-only; no review was posted and no source changes were retained.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

2 participants