Skip to content

impl code action to convert dict to TypedDict / dataclass / pydantic #2144#2483

Open
asukaminato0721 wants to merge 1 commit intofacebook:mainfrom
asukaminato0721:2144
Open

impl code action to convert dict to TypedDict / dataclass / pydantic #2144#2483
asukaminato0721 wants to merge 1 commit intofacebook:mainfrom
asukaminato0721:2144

Conversation

@asukaminato0721
Copy link
Contributor

@asukaminato0721 asukaminato0721 commented Feb 21, 2026

Summary

Fixes #2144

Implemented dict-to-model refactor code actions (TypedDict, dataclass, Pydantic) and wired them into LSP refactors, with imports and scoped insertion logic.

note: to keep simple, currently nest case is not considered(fallback to dict). Will impl in the future pr.

Test Plan

Added coverage for the new actions.

@meta-cla meta-cla bot added the cla signed label Feb 21, 2026
@asukaminato0721 asukaminato0721 marked this pull request as ready for review February 21, 2026 18:09
Copilot AI review requested due to automatic review settings February 21, 2026 18:09
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Implements a new LSP refactor code action that generates a model definition (TypedDict, dataclass, or Pydantic BaseModel) from a selected dict literal, including import insertion and basic field-type inference, and wires it into the server and test suite.

Changes:

  • Added dict_definition quick-fix implementation to generate TypedDict/dataclass/Pydantic models from dict literals.
  • Exposed the new action through Transaction and registered it in the non-wasm LSP server refactor pipeline.
  • Added test coverage for basic behavior, Any-import behavior, and rejection of non-literal keys.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pyrefly/lib/state/lsp/quick_fixes/dict_definition.rs Core implementation for dict-to-model code actions, including import insertion and type-to-annotation rendering.
pyrefly/lib/state/lsp/quick_fixes/mod.rs Registers the new dict_definition quick-fix module.
pyrefly/lib/state/lsp.rs Adds Transaction::dict_definition_code_actions API entrypoint.
pyrefly/lib/lsp/non_wasm/server.rs Wires the new refactor into the server’s refactor code action collection (with telemetry timing).
pyrefly/lib/test/lsp/code_actions.rs Adds tests validating the generated edits and titles for the new actions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +523 to +527
if let Some(first_stmt) = ast.body.iter().find(|stmt| !is_docstring_stmt(stmt)) {
first_stmt.range().start()
} else {
ast.range.end()
}
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

import_insertion_point currently inserts new imports before the first non-docstring statement. If the module begins with from __future__ import ..., this will insert imports above the future import, which is invalid in Python (future imports must come first after the module docstring/comments). Update the insertion logic to place new imports after any leading __future__ imports (and docstring), e.g. scan initial ast.body statements and advance the insertion position past all Stmt::ImportFrom where module == "__future__".

Suggested change
if let Some(first_stmt) = ast.body.iter().find(|stmt| !is_docstring_stmt(stmt)) {
first_stmt.range().start()
} else {
ast.range.end()
}
for stmt in &ast.body {
// Skip the module docstring, if present.
if is_docstring_stmt(stmt) {
continue;
}
// Skip any leading `from __future__ import ...` statements, which must
// remain the first imports after the docstring/comments.
if let Stmt::ImportFrom(import_from) = stmt {
if let Some(module) = &import_from.module {
if module.id.as_str() == "__future__" {
continue;
}
}
}
return stmt.range().start();
}
ast.range.end()

Copilot uses AI. Check for mistakes.
Comment on lines +148 to +150
if !seen_names.insert(key.clone()) {
continue;
}
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

When a dict literal has the same string key multiple times, Python keeps the last value, but this code keeps the first occurrence by continue-ing on duplicates. That can generate a model field type that doesn’t match runtime behavior. Consider either (a) overwriting the previously collected field with the later annotation, or (b) rejecting dict literals with duplicate keys so the action can’t produce a misleading schema.

Copilot uses AI. Check for mistakes.
format!("class {name}"),
format!("def {name}"),
format!("{name} ="),
format!("{name}\t="),
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

name_conflicts only checks a few raw-text patterns (e.g. "{name} =") and will miss existing bindings like annotated assignments (Data: int = 1 / Data: int) or other syntactic forms where the name is already defined. That can cause unique_name to pick a class name that collides with an existing binding. Consider expanding the patterns to include annotated-assignment forms (e.g. "{name}:") or, more robustly, deriving conflicts from the parsed AST symbol names instead of substring matching.

Suggested change
format!("{name}\t="),
format!("{name}\t="),
format!("{name}:"),
format!("{name} :"),

Copilot uses AI. Check for mistakes.
@github-actions
Copy link

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

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

code action to convert dict to TypedDict / dataclass / pydantic

2 participants