Skip to content

Commit fe04dc7

Browse files
committed
Add more refactoring tools to roadmap
1 parent d2791a6 commit fe04dc7

2 files changed

Lines changed: 272 additions & 2 deletions

File tree

docs/todo.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ with each step.
1818
| [Type Inference](todo/type-inference.md) | Generic resolution, conditional return types, type narrowing, stub attribute handling |
1919
| [Completion](todo/completion.md) | Completion-specific improvements (enum return types, array shapes, expected values) |
2020
| [Diagnostics](todo/diagnostics.md) | Scalar member access errors, chain/return member diagnostics, unknown function errors, duplicate suppression, chain error propagation, deprecated rendering, unresolved PHPDoc types, suppression intelligence, composer warnings, argument count, unreachable code, implementation errors |
21-
| [Code Actions](todo/actions.md) | Import class, remove unused imports, implement missing methods, null coalescing simplification, extract function, inline variable, extract variable, inline function/method, switch→match |
21+
| [Code Actions](todo/actions.md) | Import class, remove unused imports, implement missing methods, null coalescing simplification, extract function, extract constant, inline variable, extract variable, inline function/method, switch→match, update docblock, change visibility, generate interface |
2222
| [LSP Features](todo/lsp-features.md) | Find references, document highlighting, document/workspace symbols, rename, code lens, inlay hints, PHPDoc generation, partial result streaming, formatting proxy, file rename on class rename |
2323
| [Signature Help](todo/signature-help.md) | Parameter descriptions, signature-level docs, default values, attribute/closure support |
2424
| [Laravel](todo/laravel.md) | Model property gaps, relationship methods, type narrowing, custom builders |
@@ -109,6 +109,7 @@ gate check before Sprint 5 widens the feature surface further.
109109
| 76 | Inline Variable | Medium | Code Actions | [actions.md §7](todo/actions.md#7-inline-variable) |
110110
| 77 | Extract Variable | Medium | Code Actions | [actions.md §8](todo/actions.md#8-extract-variable) |
111111
| 78 | Inline Function/Method | High | Code Actions | [actions.md §9](todo/actions.md#9-inline-functionmethod) |
112+
| 109 | Extract Constant | Medium | Code Actions | [actions.md §10](todo/actions.md#10-extract-constant) |
112113
| 95 | Canonicalize FQN representation | High | Refactoring | [refactor.md §1](todo/refactor.md#1-canonicalize-fqn-representation) |
113114

114115
**After Sprint 4:** The core refactoring toolkit is complete. The
@@ -181,6 +182,8 @@ code action polish.
181182
| 32 | Code Lens: jump to prototype method | Low | LSP Features | [lsp-features.md §8](todo/lsp-features.md#8-code-lens-jump-to-prototype-method) |
182183
| 34 | Document Links (`textDocument/documentLink`) | Low | LSP Features | [lsp-features.md §15](todo/lsp-features.md#15-document-links-textdocumentdocumentlink) |
183184
| 39 | Simplify with null coalescing / null-safe operator (code action) | Medium | Code Actions | [actions.md §2](todo/actions.md#2-simplify-with-null-coalescing--null-safe-operator) |
185+
| 110 | Update docblock to match signature | Medium | Code Actions | [actions.md §11](todo/actions.md#11-update-docblock-to-match-signature) |
186+
| 111 | Change visibility | Low | Code Actions | [actions.md §12](todo/actions.md#12-change-visibility) |
184187

185188
---
186189

@@ -265,6 +268,7 @@ eventually but don't move the needle.
265268
| 72 | Switch → match conversion | Medium | Code Actions | [actions.md §4](todo/actions.md#4-switch--match-conversion) |
266269
| 89 | Incremental text sync | Medium | Performance | [performance.md §8](todo/performance.md#8-incremental-text-sync) |
267270
| 104 | Unreachable code diagnostic | Low | Diagnostics | [diagnostics.md §8](todo/diagnostics.md#8-unreachable-code-diagnostic) |
271+
| 112 | Generate interface from class | Medium | Code Actions | [actions.md §13](todo/actions.md#13-generate-interface-from-class) |
268272

269273
### Performance long-tail
270274

docs/todo/actions.md

Lines changed: 267 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,4 +539,270 @@ When the callee has multiple statements:
539539
| Feature | What it contributes |
540540
|---|---|
541541
| Go-to-Definition | Resolves call site to the callee's definition location and source |
542-
| Extract Function (§3) scope analysis | Variable collision detection at the call site |
542+
| Extract Function (§3) scope analysis | Variable collision detection at the call site |
543+
544+
---
545+
546+
## 10. Extract Constant
547+
**Impact: Medium · Effort: Medium**
548+
549+
Select a literal value (string, integer, float, boolean) inside a class
550+
and extract it into a class constant. This pairs naturally with Extract
551+
Variable (§8) and shares the same "select, name, replace" workflow.
552+
553+
### Behaviour
554+
555+
- **Trigger:** The user selects a literal expression inside a class
556+
method or property default. The code action introduces a new class
557+
constant with a generated name, assigns the literal value, and
558+
replaces the selection (and optionally all identical occurrences in
559+
the class) with `self::CONSTANT_NAME`.
560+
- **Code action kind:** `refactor.extract`.
561+
562+
### What can be extracted
563+
564+
- String literals: `'pending'`, `"active"`.
565+
- Integer literals: `200`, `0xFF`.
566+
- Float literals: `3.14`.
567+
- Boolean literals: `true`, `false` (less common but valid).
568+
- Concatenated string expressions: `'prefix_' . 'suffix'` — extract the
569+
whole expression as a single constant.
570+
571+
Array literals and class instantiations are out of scope (PHP const
572+
expressions are limited).
573+
574+
### Name generation
575+
576+
Generate a default name from the value:
577+
578+
- String: `'pending'``PENDING`. `'order_status'``ORDER_STATUS`.
579+
- Number: `200``STATUS_200` or `VALUE_200`.
580+
- Boolean: `true``IS_ENABLED` (weak heuristic, user will rename).
581+
- Fallback: `CONSTANT` with a numeric suffix if needed.
582+
583+
Use `SCREAMING_SNAKE_CASE` per PHP convention. If the generated name
584+
collides with an existing constant in the class, append a numeric suffix.
585+
586+
### Insertion point
587+
588+
Insert the new constant declaration at the top of the class body, after
589+
any existing constant declarations (to keep constants grouped). Use the
590+
visibility of the surrounding context as a hint: if the literal appears
591+
in a public method, default to `public const`; otherwise `private const`.
592+
593+
### Duplicate replacement
594+
595+
Same approach as Extract Variable (§8): offer "this occurrence only"
596+
and "all N occurrences in this class". Textual equality is sufficient
597+
for literals.
598+
599+
### Implementation
600+
601+
- Verify the selection is a literal expression node inside a class body.
602+
- Find the class declaration node and scan for existing constants.
603+
- Generate the constant name and check for collisions.
604+
- Determine the insertion point (after last existing constant, or at
605+
the top of the class body if none exist).
606+
- Build a `WorkspaceEdit` that:
607+
1. Inserts `{visibility} const NAME = {value};\n` at the insertion
608+
point with correct indentation.
609+
2. Replaces the selected literal with `self::NAME`.
610+
3. Optionally replaces other identical literals in the class.
611+
612+
### Prerequisites
613+
614+
| Feature | What it contributes |
615+
|---|---|
616+
| Extract Function (§3) scope analysis | Class body traversal and constant name collision detection |
617+
618+
---
619+
620+
## 11. Update Docblock to Match Signature
621+
**Impact: Medium · Effort: Medium**
622+
623+
When a function or method signature changes (parameters added, removed,
624+
reordered, or type hints updated), the docblock often falls out of sync.
625+
This code action regenerates or patches the `@param`, `@return`, and
626+
`@throws` tags to match the current signature.
627+
628+
### Behaviour
629+
630+
- **Trigger:** Cursor is on a function/method declaration that has an
631+
existing docblock. The code action appears when the docblock's `@param`
632+
tags don't match the signature's parameters (by name, count, or order),
633+
or when the `@return` tag contradicts the return type hint.
634+
- **Code action kind:** `quickfix` (when tags are clearly wrong) or
635+
`source.fixAll.docblock` for a broader sweep.
636+
637+
### What gets updated
638+
639+
1. **`@param` tags:**
640+
- Add missing `@param` for parameters present in the signature but
641+
absent from the docblock.
642+
- Remove `@param` for parameters no longer in the signature.
643+
- Reorder `@param` tags to match signature order.
644+
- Update the type if the signature has a type hint and the docblock
645+
type contradicts it (e.g. docblock says `string`, signature says
646+
`int`). If the docblock type is _more specific_ than the signature
647+
(e.g. docblock says `non-empty-string`, signature says `string`),
648+
keep the docblock type (it's a refinement, not a contradiction).
649+
- Preserve existing descriptions after the type and variable name.
650+
651+
2. **`@return` tag:**
652+
- If the signature has a return type hint and the docblock `@return`
653+
contradicts it, update the type. Same refinement rule: keep the
654+
docblock type if it's more specific.
655+
- If the signature has a return type but no `@return` tag exists,
656+
do not add one (the type hint is sufficient). Only update or
657+
remove existing tags.
658+
- Remove `@return void` if redundant with a `: void` return type.
659+
660+
3. **Preserve other tags:** `@throws`, `@template`, `@deprecated`,
661+
`@see`, and any other tags are left untouched.
662+
663+
### Edge cases
664+
665+
- **Promoted constructor parameters:** Treat the same as regular
666+
parameters for `@param` purposes.
667+
- **Variadic parameters:** `...$args` matches `@param type ...$args`.
668+
- **No existing docblock:** This action only patches existing docblocks.
669+
PHPDoc generation on `/**` (Sprint 5, item 24) handles creating new
670+
ones.
671+
672+
### Implementation
673+
674+
- Parse the function signature to extract parameter names, types, and
675+
order, plus the return type.
676+
- Parse the existing docblock to extract `@param` and `@return` tags
677+
with their positions, types, variable names, and descriptions.
678+
- Diff the two lists to determine additions, removals, reorderings,
679+
and type updates.
680+
- Build a `WorkspaceEdit` with targeted `TextEdit`s that modify only
681+
the changed lines within the docblock, preserving formatting,
682+
indentation, and unchanged tags.
683+
684+
### Prerequisites
685+
686+
| Feature | What it contributes |
687+
|---|---|
688+
| Docblock tag parsing (`docblock/tags.rs`) | Extracts existing `@param`/`@return` tags with positions |
689+
| Parser (`parser/functions.rs`) | Extracts parameter names, types, and return type from the signature |
690+
691+
---
692+
693+
## 12. Change Visibility
694+
**Impact: Low-Medium · Effort: Low**
695+
696+
Change the visibility of a method, property, constant, or class from the
697+
cursor position. Offers all applicable alternatives as separate code
698+
actions.
699+
700+
### Behaviour
701+
702+
- **Trigger:** Cursor is on (or inside) a method, property, constant, or
703+
promoted constructor parameter that has an explicit visibility modifier.
704+
- **Code action kind:** `refactor.rewrite`.
705+
- **Offered actions:** One action per alternative visibility. For a
706+
`public` method, offer "Make protected" and "Make private". For a
707+
`private` property, offer "Make protected" and "Make public".
708+
709+
### What can be changed
710+
711+
- Methods: `public``protected``private`.
712+
- Properties (including promoted constructor parameters): same.
713+
- Constants: `public``protected``private` (PHP 8.1+).
714+
- Constructor visibility promotion: `public function __construct(private string $name)`
715+
change `private` to `protected` or `public`.
716+
717+
### Scope
718+
719+
This is a single-file edit. It does **not** update call sites or
720+
subclass overrides in other files. If a user makes a public method
721+
private and there are external callers, they'll see errors from
722+
diagnostics or their static analyser. Cross-file visibility propagation
723+
is a possible follow-up but not required for the initial implementation.
724+
725+
### Implementation
726+
727+
- Find the visibility keyword token for the member under the cursor.
728+
- Determine the current visibility.
729+
- For each alternative visibility, create a code action whose
730+
`WorkspaceEdit` replaces the visibility keyword token with the new one.
731+
- Handle the edge case of implicit public visibility (no keyword present)
732+
by inserting the keyword before the `function`/property token.
733+
734+
### Prerequisites
735+
736+
None beyond basic AST navigation. This is one of the simplest possible
737+
code actions.
738+
739+
---
740+
741+
## 13. Generate Interface from Class
742+
**Impact: Low-Medium · Effort: Medium**
743+
744+
Extract an interface from an existing class. The new interface contains
745+
method signatures for all public methods in the class, and the class is
746+
updated to implement it.
747+
748+
### Behaviour
749+
750+
- **Trigger:** Cursor is on a class declaration. The code action
751+
"Extract interface" appears.
752+
- **Code action kind:** `refactor.extract`.
753+
- **Result:** A new file is created containing the interface, and the
754+
original class is updated to add `implements InterfaceName`.
755+
756+
### What gets extracted
757+
758+
- All `public` methods (excluding the constructor) become interface
759+
method signatures: visibility, name, parameters with types and
760+
defaults, and return type.
761+
- PHPDoc blocks from the extracted methods are copied to the interface
762+
(they often contain `@param`, `@return`, and `@template` tags that
763+
are essential for type information).
764+
- Class-level `@template` tags are copied if any extracted method
765+
references those template parameters.
766+
- Public constants are **not** extracted (interface constants have
767+
different semantics and this is rarely what users want).
768+
769+
### Naming
770+
771+
Default interface name: `{ClassName}Interface`. Place it in the same
772+
namespace and directory as the class. If the file uses PSR-4, the
773+
interface file path is derived from the namespace.
774+
775+
### Implementation
776+
777+
- Parse the class to collect public method signatures and their
778+
docblocks.
779+
- Collect class-level `@template` tags if referenced by extracted
780+
methods.
781+
- Generate the interface source: namespace declaration, use imports
782+
needed by the method signatures, interface declaration with method
783+
stubs.
784+
- Build a `WorkspaceEdit` with two operations:
785+
1. `CreateFile` + `TextEdit` for the new interface file.
786+
2. `TextEdit` on the original class to add `implements InterfaceName`
787+
(and a `use` import if the interface is in a different file, though
788+
by default it's the same namespace).
789+
- Format the generated interface to match the project's indentation
790+
style (detect from the source class).
791+
792+
### Edge cases
793+
794+
- **Class already implements interfaces:** Append to the existing
795+
`implements` list rather than replacing it.
796+
- **Abstract methods:** Include them in the interface (they're already
797+
stubs).
798+
- **Static methods:** Include them. Interfaces can declare static method
799+
signatures.
800+
- **Generic classes:** If the class has `@template T` and a method
801+
returns `T`, the interface needs the same `@template` tag.
802+
803+
### Prerequisites
804+
805+
| Feature | What it contributes |
806+
|---|---|
807+
| Parser (`parser/classes.rs`) | Extracts public method signatures with full type information |
808+
| Implement missing methods (§1) | Shared infrastructure for generating method stubs and `implements` clause editing |

0 commit comments

Comments
 (0)