Skip to content

impl Implemented encapsulate_field #2207#2691

Open
asukaminato0721 wants to merge 3 commits into
facebook:mainfrom
asukaminato0721:2207-encapsulate_field
Open

impl Implemented encapsulate_field #2207#2691
asukaminato0721 wants to merge 3 commits into
facebook:mainfrom
asukaminato0721:2207-encapsulate_field

Conversation

@asukaminato0721

@asukaminato0721 asukaminato0721 commented Mar 6, 2026

Copy link
Copy Markdown
Contributor

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

Copilot AI left a comment

Copy link
Copy Markdown

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;
}

Copilot AI Mar 6, 2026

Copy link

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)?;

Copilot AI Mar 6, 2026

Copy link

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.
Comment thread pyrefly/lib/state/lsp/quick_fixes/encapsulate_field.rs Outdated
assert_eq!(expected.trim(), updated.trim());
}

#[test]

Copilot AI Mar 6, 2026

Copy link

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);
}

Copilot AI Mar 6, 2026

Copy link

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
@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

This comment has been minimized.

@asukaminato0721

Copy link
Copy Markdown
Contributor Author

@jvansch1 this can be reviewed

@github-actions

This comment has been minimized.

@asukaminato0721 asukaminato0721 force-pushed the 2207-encapsulate_field branch from 70b0f39 to 37ebf0a Compare March 27, 2026 23:24
@github-actions github-actions Bot added size/xl and removed size/xl labels Mar 27, 2026
@github-actions

This comment has been minimized.

@github-actions github-actions Bot added the stale label Apr 12, 2026
@jvansch1

Copy link
Copy Markdown
Contributor

@asukaminato0721 Looks like there are a lot of failing checks. Once these are fixed I can do a review.

@jvansch1

Copy link
Copy Markdown
Contributor

@asukaminato0721 As part of the description of this PR could you also add either a video showing this new functionality or instructions for how to test it?

@asukaminato0721 asukaminato0721 force-pushed the 2207-encapsulate_field branch from 37ebf0a to c310643 Compare April 20, 2026 14:48
@github-actions

This comment has been minimized.

@asukaminato0721

Copy link
Copy Markdown
Contributor Author
Screencast_20260421_012325.webm

@asukaminato0721 asukaminato0721 force-pushed the 2207-encapsulate_field branch from c310643 to af4e7a1 Compare April 25, 2026 19:38
@github-actions github-actions Bot added size/xl and removed size/xl labels Apr 25, 2026
@github-actions

This comment has been minimized.

@github-actions github-actions Bot removed the stale label Apr 27, 2026
@github-actions

This comment has been minimized.

@github-actions github-actions Bot added the stale label May 14, 2026
@asukaminato0721 asukaminato0721 force-pushed the 2207-encapsulate_field branch 2 times, most recently from d225587 to c98fce0 Compare May 14, 2026 03:09
@github-actions github-actions Bot added size/xl and removed size/xl labels May 14, 2026
@github-actions

This comment has been minimized.

@github-actions github-actions Bot removed the stale label May 17, 2026
@github-actions

This comment has been minimized.

@github-actions github-actions Bot added the stale label May 31, 2026
• 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 c98fce0 to a760174 Compare May 31, 2026 06:00
@github-actions github-actions Bot added size/xl and removed size/xl labels May 31, 2026
@github-actions

Copy link
Copy Markdown

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

@github-actions github-actions Bot removed the stale label Jun 1, 2026
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.

3 participants