Skip to content

Commit b681baf

Browse files
committed
Update roadmap
1 parent 739cb16 commit b681baf

3 files changed

Lines changed: 363 additions & 143 deletions

File tree

docs/todo.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,14 @@ within the same impact tier.
2121

2222
## Sprint 4 — Refactoring toolkit
2323

24-
| # | Item | Impact | Effort |
25-
| --- | --------------------------------------------------------------------------------- | ------ | ------ |
26-
| | Clear [refactoring gate](todo/refactor.md) |||
27-
| A1 | [Extract method](todo/actions.md#a1-extract-method) | High | High |
28-
| A2 | [Extract variable](todo/actions.md#a2-extract-variable) | Medium | Medium |
29-
| A4 | [Inline variable](todo/actions.md#a4-inline-variable) | Medium | Medium |
30-
| A5 | [Change visibility](todo/actions.md#a5-change-visibility) | Medium | Low |
31-
| A6 | [Generate constructor](todo/actions.md#a6-generate-constructor) | Medium | Medium |
32-
| A7 | [Promote constructor parameter](todo/actions.md#a7-promote-constructor-parameter) | Medium | Low |
24+
| # | Item | Impact | Effort |
25+
| --- | ---------------------------------------------------------------------------------- | ------ | ------ |
26+
| R1 | [Extract cursor-context AST helper](todo/refactor.md#r1-cursor-context-ast-helper) || Low |
27+
| A1 | [Extract method](todo/actions.md#a1-extract-method) | High | High |
28+
| A2 | [Extract variable](todo/actions.md#a2-extract-variable) | Medium | Medium |
29+
| A4 | [Inline variable](todo/actions.md#a4-inline-variable) | Medium | Medium |
30+
| A6 | [Generate constructor](todo/actions.md#a6-generate-constructor) | Medium | Medium |
31+
| A7 | [Promote constructor parameter](todo/actions.md#a7-promote-constructor-parameter) | Medium | Low |
3332

3433
## Sprint 5 — Polish for office adoption
3534

docs/todo/actions.md

Lines changed: 188 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Constant) all depend on. It provides forward-pass variable usage
1515
tracking with byte offsets across function scopes.
1616

1717
## A1. Simplify with null coalescing / null-safe operator
18+
1819
**Impact: Medium · Effort: Medium**
1920

2021
Offer code actions to simplify common nullable patterns:
@@ -43,6 +44,7 @@ if-statement patterns are a follow-up.
4344
---
4445

4546
## A11. ScopeCollector infrastructure
47+
4648
**Impact: Medium · Effort: Medium**
4749

4850
A lightweight forward-pass AST walker that collects every variable
@@ -112,14 +114,15 @@ once and produces a `ScopeMap` containing:
112114

113115
### Prerequisites
114116

115-
| Feature | What it contributes |
116-
|---|---|
117+
| Feature | What it contributes |
118+
| --------------------------------------- | ------------------------------------------------------------- |
117119
| Find References (see `lsp-features.md`) | Variable usage tracking across a scope — overlapping analysis |
118-
| Implement missing methods (shipped) | Validates the code action + `WorkspaceEdit` plumbing |
120+
| Implement missing methods (shipped) | Validates the code action + `WorkspaceEdit` plumbing |
119121

120122
---
121123

122124
## A2. Extract Function refactoring
125+
123126
**Impact: Low-Medium · Effort: High**
124127

125128
Select a range of statements inside a method/function and extract them into a
@@ -143,16 +146,17 @@ class, not a standalone function.
143146

144147
### Prerequisites (build these first)
145148

146-
| Feature | What it contributes |
147-
|---|---|
148-
| ScopeCollector (A11) | Variable read/write tracking with byte offsets, range partitioning |
149-
| Hover | "Resolve type at arbitrary position" — needed to type params |
149+
| Feature | What it contributes |
150+
| ---------------------------------------- | ----------------------------------------------------------------------------------------- |
151+
| ScopeCollector (A11) | Variable read/write tracking with byte offsets, range partitioning |
152+
| Hover | "Resolve type at arbitrary position" — needed to type params |
150153
| Document Symbols (see `lsp-features.md`) | AST range → symbol mapping — needed to find enclosing function and valid insertion points |
151-
| Implement missing methods (shipped) | Builds the code action + `WorkspaceEdit` plumbing |
154+
| Implement missing methods (shipped) | Builds the code action + `WorkspaceEdit` plumbing |
152155

153156
---
154157

155158
## A3. Switch → match conversion
159+
156160
**Impact: Low · Effort: Medium**
157161

158162
Offer a code action to convert a `switch` statement to a `match`
@@ -191,6 +195,7 @@ but bounded in scope.
191195
---
192196

193197
## A4. Inline Variable
198+
194199
**Impact: Medium · Effort: Medium**
195200

196201
Replace every occurrence of a variable with its right-hand-side
@@ -251,13 +256,14 @@ Before offering the action, verify:
251256

252257
### Prerequisites
253258

254-
| Feature | What it contributes |
255-
|---|---|
259+
| Feature | What it contributes |
260+
| -------------------- | ---------------------------------------------- |
256261
| ScopeCollector (A11) | Variable read/write tracking with byte offsets |
257262

258263
---
259264

260265
## A5. Extract Variable
266+
261267
**Impact: Medium · Effort: Medium**
262268

263269
Select an expression and extract it into a new local variable assigned
@@ -321,13 +327,14 @@ insert before the `if` statement, not inside the condition.
321327

322328
### Prerequisites
323329

324-
| Feature | What it contributes |
325-
|---|---|
330+
| Feature | What it contributes |
331+
| -------------------- | --------------------------------------------------------------- |
326332
| ScopeCollector (A11) | Enclosing scope detection and variable name collision avoidance |
327333

328334
---
329335

330336
## A6. Inline Function/Method
337+
331338
**Impact: Medium · Effort: High**
332339

333340
Replace a function or method call with the body of the called function,
@@ -420,14 +427,15 @@ When the callee has multiple statements:
420427

421428
### Prerequisites
422429

423-
| Feature | What it contributes |
424-
|---|---|
425-
| Go-to-Definition | Resolves call site to the callee's definition location and source |
426-
| ScopeCollector (A11) | Variable collision detection at the call site |
430+
| Feature | What it contributes |
431+
| -------------------- | ----------------------------------------------------------------- |
432+
| Go-to-Definition | Resolves call site to the callee's definition location and source |
433+
| ScopeCollector (A11) | Variable collision detection at the call site |
427434

428435
---
429436

430437
## A7. Extract Constant
438+
431439
**Impact: Medium · Effort: Medium**
432440

433441
Select a literal value (string, integer, float, boolean) inside a class
@@ -495,13 +503,14 @@ for literals.
495503

496504
### Prerequisites
497505

498-
| Feature | What it contributes |
499-
|---|---|
506+
| Feature | What it contributes |
507+
| -------------------- | ---------------------------------------------------------- |
500508
| ScopeCollector (A11) | Class body traversal and constant name collision detection |
501509

502510
---
503511

504512
## A8. Update Docblock to Match Signature
513+
505514
**Impact: Medium · Effort: Medium**
506515

507516
When a function or method signature changes (parameters added, removed,
@@ -566,62 +575,15 @@ This code action regenerates or patches the `@param`, `@return`, and
566575

567576
### Prerequisites
568577

569-
| Feature | What it contributes |
570-
|---|---|
571-
| Docblock tag parsing (`docblock/tags.rs`) | Extracts existing `@param`/`@return` tags with positions |
572-
| Parser (`parser/functions.rs`) | Extracts parameter names, types, and return type from the signature |
573-
574-
---
575-
576-
## A9. Change Visibility
577-
**Impact: Low-Medium · Effort: Low**
578-
579-
Change the visibility of a method, property, constant, or class from the
580-
cursor position. Offers all applicable alternatives as separate code
581-
actions.
582-
583-
### Behaviour
584-
585-
- **Trigger:** Cursor is on (or inside) a method, property, constant, or
586-
promoted constructor parameter that has an explicit visibility modifier.
587-
- **Code action kind:** `refactor.rewrite`.
588-
- **Offered actions:** One action per alternative visibility. For a
589-
`public` method, offer "Make protected" and "Make private". For a
590-
`private` property, offer "Make protected" and "Make public".
591-
592-
### What can be changed
593-
594-
- Methods: `public``protected``private`.
595-
- Properties (including promoted constructor parameters): same.
596-
- Constants: `public``protected``private` (PHP 8.1+).
597-
- Constructor visibility promotion: `public function __construct(private string $name)`
598-
change `private` to `protected` or `public`.
599-
600-
### Scope
601-
602-
This is a single-file edit. It does **not** update call sites or
603-
subclass overrides in other files. If a user makes a public method
604-
private and there are external callers, they'll see errors from
605-
diagnostics or their static analyser. Cross-file visibility propagation
606-
is a possible follow-up but not required for the initial implementation.
607-
608-
### Implementation
609-
610-
- Find the visibility keyword token for the member under the cursor.
611-
- Determine the current visibility.
612-
- For each alternative visibility, create a code action whose
613-
`WorkspaceEdit` replaces the visibility keyword token with the new one.
614-
- Handle the edge case of implicit public visibility (no keyword present)
615-
by inserting the keyword before the `function`/property token.
616-
617-
### Prerequisites
618-
619-
None beyond basic AST navigation. This is one of the simplest possible
620-
code actions.
578+
| Feature | What it contributes |
579+
| ----------------------------------------- | ------------------------------------------------------------------- |
580+
| Docblock tag parsing (`docblock/tags.rs`) | Extracts existing `@param`/`@return` tags with positions |
581+
| Parser (`parser/functions.rs`) | Extracts parameter names, types, and return type from the signature |
621582

622583
---
623584

624585
## A10. Generate Interface from Class
586+
625587
**Impact: Low-Medium · Effort: Medium**
626588

627589
Extract an interface from an existing class. The new interface contains
@@ -685,7 +647,159 @@ interface file path is derived from the namespace.
685647

686648
### Prerequisites
687649

688-
| Feature | What it contributes |
689-
|---|---|
690-
| Parser (`parser/classes.rs`) | Extracts public method signatures with full type information |
691-
| Implement missing methods (shipped) | Shared infrastructure for generating method stubs and `implements` clause editing |
650+
| Feature | What it contributes |
651+
| ----------------------------------- | --------------------------------------------------------------------------------- |
652+
| Parser (`parser/classes.rs`) | Extracts public method signatures with full type information |
653+
| Implement missing methods (shipped) | Shared infrastructure for generating method stubs and `implements` clause editing |
654+
655+
---
656+
657+
## A6. Generate Constructor
658+
659+
**Impact: Medium · Effort: Medium**
660+
661+
When a class has properties but no constructor, offer a code action to
662+
generate a constructor that accepts each property as a parameter and
663+
assigns it.
664+
665+
### Behaviour
666+
667+
- **Trigger:** Cursor is anywhere inside a class body that has at least
668+
one non-static property and no existing `__construct` method.
669+
- **Code action kind:** `refactor.rewrite`.
670+
- **Result:** A `__construct` method is inserted as the first method in
671+
the class body. Each non-static property becomes a parameter with a
672+
matching type hint (taken from the property's type declaration or
673+
`@var` docblock tag). The body assigns each parameter to the
674+
corresponding property: `$this->name = $name;`.
675+
676+
### What gets included
677+
678+
- All non-static, non-readonly properties, in declaration order.
679+
- Properties with a native type declaration get a matching parameter
680+
type hint. Nullable properties (`?Foo`, `Foo|null`) produce a
681+
nullable parameter.
682+
- Properties with only a `@var` docblock type (no native hint) get the
683+
docblock type as the parameter type hint if it is a single,
684+
non-compound type; otherwise the parameter is untyped.
685+
- Properties that already have a default value in their declaration get
686+
a matching default value on the parameter (e.g.
687+
`public string $status = 'active'``string $status = 'active'`).
688+
- `readonly` properties are excluded — they cannot be assigned in a
689+
constructor body after PHP 8.1 (use constructor promotion for those;
690+
see A7).
691+
- Static properties are excluded.
692+
693+
### Insertion point
694+
695+
Insert the constructor as the first method in the class body, after any
696+
property declarations, at the same indentation level as the other
697+
members. Detect indentation from the existing class body.
698+
699+
### Implementation
700+
701+
- Use the shared cursor-context helper (see refactor.md) to find the
702+
class the cursor is inside. Confirm there is no existing
703+
`__construct` method — if one exists, do not offer the action.
704+
- Walk the class members to collect qualifying properties in declaration
705+
order.
706+
- Build the parameter list and assignment body from the collected
707+
properties.
708+
- Find the insertion offset: the byte offset of the closing `}` of the
709+
last property declaration, or the opening `{` of the class body if
710+
there are no properties before any existing methods.
711+
- Build a `WorkspaceEdit` that inserts the generated constructor text at
712+
the insertion offset.
713+
714+
### Edge cases
715+
716+
- **No qualifying properties:** Do not offer the action if there are no
717+
non-static, non-readonly properties to generate parameters for.
718+
- **Class already has a constructor:** Do not offer the action.
719+
- **Abstract class:** Offer the action — abstract classes can have
720+
constructors.
721+
- **Union types:** Preserve union type hints as-is on the parameter
722+
(e.g. `int|string $id`).
723+
724+
### Prerequisites
725+
726+
| Feature | What it contributes |
727+
| --------------------------------------------- | ---------------------------------------------------------- |
728+
| Cursor-context AST helper (R1 in refactor.md) | Locating the enclosing class and its members at the cursor |
729+
| `detect_class_indent` (implement_methods.rs) | Detecting the indentation style to use in generated code |
730+
731+
---
732+
733+
## A7. Promote Constructor Parameter
734+
735+
**Impact: Medium · Effort: Low**
736+
737+
Convert a regular constructor parameter + property assignment pair into
738+
a single constructor-promoted property.
739+
740+
### Behaviour
741+
742+
- **Trigger:** Cursor is on a constructor parameter that has a
743+
corresponding `$this->name = $name;` assignment in the constructor
744+
body, and a matching property declaration in the class body.
745+
- **Code action kind:** `refactor.rewrite`.
746+
- **Result:** The property declaration is removed, the assignment
747+
statement is removed, and the parameter gains a visibility modifier
748+
(`public`, `protected`, or `private` — matching the original
749+
property's visibility, defaulting to `public` if undetectable).
750+
751+
### What qualifies
752+
753+
A parameter qualifies for promotion when all of the following hold:
754+
755+
1. It is a parameter of `__construct`.
756+
2. A property exists in the class with the same name.
757+
3. The constructor body contains exactly one assignment of the form
758+
`$this->name = $name;` for that parameter (no more, no less — if
759+
the parameter is used for anything else in the body, promotion would
760+
change semantics).
761+
4. The property is not `static`.
762+
5. The property is not already promoted.
763+
764+
### Scope
765+
766+
Single-file edit only. Does not affect call sites (promotion is
767+
transparent to callers).
768+
769+
### Implementation
770+
771+
- Use the shared cursor-context helper (see refactor.md) to confirm the
772+
cursor is on a `__construct` parameter.
773+
- Walk the class members to find the matching property declaration and
774+
record its visibility and byte span.
775+
- Walk the constructor body to find the matching assignment statement
776+
and record its byte span (full statement including leading whitespace
777+
and trailing newline).
778+
- Verify that the parameter is not used in any other way in the
779+
constructor body (simple scan: if `$paramName` appears more than once
780+
in the constructor body, reject).
781+
- Build a `WorkspaceEdit` with three edits:
782+
1. Delete the property declaration.
783+
2. Delete the assignment statement.
784+
3. Insert the visibility modifier before the parameter's type hint in
785+
the parameter list (e.g. `string $name``private string $name`).
786+
787+
### Edge cases
788+
789+
- **Parameter used elsewhere in constructor body:** Do not offer the
790+
action (inlining would change semantics or leave the body broken).
791+
- **Property has a docblock:** Drop it — promoted properties can have
792+
docblocks on the parameter itself, but the property-level docblock
793+
is typically just `@var` which becomes redundant. The user can add
794+
one manually if needed.
795+
- **Property has a default value:** Carry it over to the promoted
796+
parameter default (e.g. `public string $status = 'active'`).
797+
- **Readonly property:** Promote as `public readonly string $name` (or
798+
whatever visibility the original property had). PHP 8.1+ supports
799+
`readonly` on promoted parameters.
800+
801+
### Prerequisites
802+
803+
| Feature | What it contributes |
804+
| --------------------------------------------- | ------------------------------------------------------ |
805+
| Cursor-context AST helper (R1 in refactor.md) | Locating the enclosing class and constructor parameter |

0 commit comments

Comments
 (0)