@@ -15,6 +15,7 @@ Constant) all depend on. It provides forward-pass variable usage
1515tracking with byte offsets across function scopes.
1616
1717## A1. Simplify with null coalescing / null-safe operator
18+
1819** Impact: Medium · Effort: Medium**
1920
2021Offer 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
4850A 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
125128Select 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
158162Offer 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
196201Replace 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
263269Select 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
333340Replace 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
433441Select 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
507516When 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
627589Extract 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