Skip to content

impl Implemented encapsulate_field #2207#2691

Open
asukaminato0721 wants to merge 2 commits intofacebook:mainfrom
asukaminato0721:2207-encapsulate_field
Open

impl Implemented encapsulate_field #2207#2691
asukaminato0721 wants to merge 2 commits intofacebook:mainfrom
asukaminato0721:2207-encapsulate_field

Conversation

@asukaminato0721
Copy link
Contributor

@asukaminato0721 asukaminato0721 commented Mar 6, 2026

Summary

Fixes part of #2207

Implemented encapsulate_field as a new local rewrite refactor. It now offers a code action on attribute selections, inserts unique get_<field> / set_<field> methods, rewrites reads to getter calls, rewrites assignments and += to setter calls, and rejects unsupported tuple-assignment targets. It’s wired into the transaction and LSP code-action pipeline.

ref https://rope.readthedocs.io/en/latest/library.html#list-of-refactorings

Test Plan

Added focused tests for read/write rewriting, unique accessor naming, and tuple-target rejection.

@meta-cla meta-cla bot added the cla signed label Mar 6, 2026
@github-actions

This comment has been minimized.

@asukaminato0721 asukaminato0721 marked this pull request as ready for review March 6, 2026 17:44
Copilot AI review requested due to automatic review settings March 6, 2026 17:44
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

Adds a new local “Encapsulate field” refactor to the Pyrefly LSP pipeline, generating getter/setter methods on a class field and rewriting local reads/writes to use those accessors.

Changes:

  • Implemented encapsulate_field local refactor (generate accessor methods + rewrite reads/writes and +=).
  • Wired the refactor into Transaction and the non-wasm LSP server code-action path.
  • Added focused LSP code-action tests for read/write rewriting, unique naming, and tuple-target rejection.

Reviewed changes

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

Show a summary per file
File Description
pyrefly/lib/state/lsp/quick_fixes/encapsulate_field.rs New refactor implementation (edits generation + method insertion).
pyrefly/lib/state/lsp/quick_fixes/mod.rs Exposes the new quick-fix module.
pyrefly/lib/state/lsp.rs Adds Transaction::encapsulate_field_code_actions entrypoint.
pyrefly/lib/lsp/non_wasm/server.rs Adds the refactor to the server’s timed refactor action pipeline.
pyrefly/lib/test/lsp/code_actions.rs Adds tests for the new refactor behavior.

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

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +92 to +102
for reference in references {
if handled_stmt_ranges
.iter()
.any(|stmt_range: &TextRange| stmt_range.contains(reference.start()))
{
continue;
}
let occurrence = classify_occurrence(ast.as_ref(), reference)?;
if reference == definition.definition_range && occurrence.is_definition_write() {
continue;
}
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

handled_stmt_ranges is checked before classifying the occurrence, so once a write in a statement is handled (usually the LHS, since references are sorted by start position), all later references in the same statement are skipped. This leaves un-rewritten reads in RHS expressions (e.g. self.value = self.value becomes self.set_value(self.value)), breaking encapsulation. Consider classifying first, then only dedup write occurrences per statement (or track handled write-target ranges instead of entire statement ranges).

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +76
let definition = transaction
.find_definition(handle, position, FindPreference::default())
.into_iter()
.find(|definition| {
definition.module.path() == module_info.path()
&& matches!(
definition.metadata,
DefinitionMetadata::Attribute | DefinitionMetadata::VariableOrAttribute(_)
)
&& !matches!(
definition.metadata.symbol_kind(),
Some(
SymbolKind::Module
| SymbolKind::Function
| SymbolKind::Method
| SymbolKind::Class
)
)
})?;
let ast = transaction.get_ast(handle)?;
let class_def = enclosing_class(ast.as_ref(), definition.definition_range)?;
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

The definition filter attempts to exclude methods/functions/classes via definition.metadata.symbol_kind(), but DefinitionMetadata::Attribute always reports SymbolKind::Attribute, even when the resolved attribute is actually a method/property. As a result, this refactor can be offered on obj.method and will rewrite obj.method() into something invalid like obj.get_method()(). Suggest explicitly rejecting attribute definitions whose definition_range is within a Stmt::FunctionDef/Stmt::ClassDef (via Ast::locate_node), or otherwise verifying the definition comes from an assignment/AnnAssign to a field rather than a callable/class member.

Copilot uses AI. Check for mistakes.
assert_eq!(expected.trim(), updated.trim());
}

#[test]
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

Current tests don’t cover the case where a write statement also contains reads of the same attribute (e.g. self.value = self.value or self.value += self.value). Given references are processed in source order, this scenario currently risks leaving RHS reads un-rewritten; adding a regression test here would prevent this from reappearing.

Suggested change
#[test]
#[test]
fn encapsulate_field_rewrites_rhs_reads_in_write() {
let code = r#"
class Counter:
def __init__(self, value):
self.value = value
def copy(self):
self.value = self.value
# ^
def double(self):
self.value += self.value
"#;
let updated =
apply_first_encapsulate_field_action(code).expect("expected encapsulate field action");
let expected = r#"
class Counter:
def __init__(self, value):
self.value = value
def copy(self):
self.set_value(self.get_value())
def double(self):
self.set_value(self.get_value() + self.get_value())
def get_value(self):
return self.value
def set_value(self, value):
self.value = value
# ^
"#;
assert_eq!(expected.trim(), updated.trim());
}
#[test]

Copilot uses AI. Check for mistakes.
# ^
"#;
assert_no_encapsulate_field_action(code);
}
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

Consider adding a test ensuring encapsulate-field is not offered for method attributes (e.g. cursor on obj.method / obj.method()), since the refactor currently treats any resolved DefinitionMetadata::Attribute as a field and could generate invalid rewrites for callables/properties.

Suggested change
}
}
#[test]
fn encapsulate_field_not_offered_for_method_attribute() {
let code = r#"
class Counter:
def __init__(self, value):
self.value = value
def inc(self):
self.value += 1
def use(counter: Counter):
method_ref = counter.inc
# ^
"#;
assert_no_encapsulate_field_action(code);
}
#[test]
fn encapsulate_field_not_offered_for_method_call() {
let code = r#"
class Counter:
def __init__(self, value):
self.value = value
def inc(self):
self.value += 1
def use(counter: Counter):
counter.inc()
# ^
"#;
assert_no_encapsulate_field_action(code);
}

Copilot uses AI. Check for mistakes.
@asukaminato0721 asukaminato0721 marked this pull request as draft March 6, 2026 19:20
• Implemented encapsulate_field as a new local rewrite refactor in
encapsulate_field.rs:42. It now offers a code action on attribute
selections, inserts unique get_<field> / set_<field>
  methods, rewrites reads to getter calls, rewrites assignments and +=
to setter calls, and rejects unsupported tuple-assignment targets. It’s
wired into the transaction and LSP code-
  action pipeline in lsp.rs:2352 and server.rs:4093.

  Added focused tests in code_actions.rs:3599 for read/write rewriting,
unique accessor naming, and tuple-target rejection.
@asukaminato0721 asukaminato0721 force-pushed the 2207-encapsulate_field branch from 4897879 to 1af67f5 Compare March 6, 2026 19:44
@asukaminato0721 asukaminato0721 marked this pull request as ready for review March 6, 2026 19:59
@github-actions
Copy link

github-actions bot commented Mar 6, 2026

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.

2 participants